From a23fa7fc66110234d83ab0fd5882bce6fb79f024 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 23 May 2022 16:02:37 +0800 Subject: [PATCH 001/224] 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 002/224] 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 003/224] 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 zcmeHQc|26z|35SKE&G-*mXah&B~fFkXr)DEO&hIfqby^T&>|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 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 004/224] 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 005/224] 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 006/224] 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 007/224] 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 008/224] 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 009/224] 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 010/224] 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 011/224] 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 012/224] 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 013/224] 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 014/224] 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 015/224] 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 016/224] 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 017/224] 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 018/224] 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 019/224] 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 020/224] 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 021/224] 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 022/224] 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 023/224] 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 024/224] 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 025/224] 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 026/224] 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 027/224] 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 028/224] 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 029/224] 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 030/224] 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 031/224] 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 032/224] 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 033/224] 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 034/224] 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 035/224] 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 036/224] 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 037/224] 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 038/224] 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 039/224] 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 040/224] 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 041/224] 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 042/224] 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 043/224] 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 044/224] 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 045/224] 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 zcmeHQYjjn`6+X21sH>~;LjhNR(017+wusg~WVI@7|MYKLi&BenZ}||4hPo`dj8GM=!x1bAC)zl25c_JyFePM2#niE}x5hFPR*@)-6xj_Yv*fK-7GJ z=-OqXllzH2D<@i0Nc8$(BEJ6!?QA}_OmzYBug@Ir%e{#HHJ9l49`M#oapmSsqNQVF z@rr(DUU#gGo0M)`BWgHBw5^nAB6Q4)KXKDFe|~ETkpC!l{*qge-mA4 zjL)0%FFQ86-}hzs{2MLKtKH*YUMD)RBce;34~=voE%<-+Vff`OMAu=HUggkEcOO3G zCtmMFbn{E1QwNEPMror1smp&``|bT=*n5+AuIv;i_7V+en{|YlzqYRs_=Fzp-9|L! zcka67e3p;#l;wZwZ((bP1h{b|IalM(f6)t$3k9Ohm(ULRM^b%3%JDxGeXtc%VDo5` ztB`p7FPo_1?vZ=!`;chN{YlY-l;S@FKEGlG>e(CWxH)^o-TqghkEc=^(C>WhiH`M* zkG`Mlw3OkW`(vM=t*%;5p-=Bt|F52>`bk8W&@Y+$QlM>ul;A(}Yec&@tLKC7Si8%e zk7c?3JdejCiZTQ91EJ@CIDG!cKG?m@ZuNiN`7ijDeTDkZD;MDhUW5*yEo1zD=NN1) zQICJ@9V=J6KcDM_OBg5iORho(rVe(0wnCi>B&I&k`N}7-NeG zjs<(o;r{=j{n<8_CCG7Sbx0`r&qTd=qS`BxonPNn7imW zb@LNnFAz^pxNVVfn^GMJ4gc&s|AF{ts#QsQBculFGy*tx|g8yRl zqeO*q9)&izBD|E{XR$oRZWFPhH8q~MCao?IB)B}ewZY(N! zKUGpL?fK_+|HboSC|yFjOm%G~<$6>+PP{+6@vZdWe>&F4iuRefp7b(~AGDuf-`03q zOt-7I6SeLokkfDN?SDDO=0#=lFO}Gca=W+njCJ#)bz-Hvj5X~F`hnW>U;d65K6QuT z`@gO?|EveI28)&NdNWb(!_wu`p8tK=HzgvwHcMmUo88;?&!4=;;40?#yb8}JEP7SC zjC%8rHgav1S7ajBuX#fnn?U&I{pQE^ij{HYOzARe%|Bzq`!Bo-&y~#|CXG!X{Id_( zQ|YyYk?YGANtaP?{;`(giom!Z_ZM>RmBuD8{{@2d)TEfWn3dT6}_W#`@ z=W%&OhueRWb`fa$vc+CY7`c8s<~JnEsWtz+MzdC6%^&ywavzk&Mi$$I(~It3-B=`D zM!osxu@TI3c@-Z2S@?=HHVN}5X@hh`aE!B*&yJDn=V;Hr=w6>q@Trp61d@K#U8v(u zde!jAx`72*(q+`1|D}1rMxYOH=u>HI0^xu1&%JZcDfpa-3H_wYs6GD^et@zH?DK8L z{`uDR-g5rYcO59hIvx?Zh;i{3<;$i$|6Im%Si>nI>olJOD}?=MGIHi9_B4D$ zY2HJ{KXW$s1=N9<-;t4~GemQq!`?7;b`)11_{<0HgSH5)V`x`K_`TA6rQCC#L&rb2 z1NLldhm25!=ZN25jDD1lIxzNryypY+mnzPIJBdD@4`W%OI=oBK}k%-?B zQal!CtKq&X>I5FI%hLV5gX~k=5?27qe`{e%k+Y>NW zg!xzzJ>a%U?$09cdy}o_ojOw~zs^5inL^7ypM|(b^jruhz%g+?2X@y7L~o*>#_y8p zN*A)f8#&wUXqEVQUd-b=d_8@{*E0-4%Rlonx)0Im!yO<~@&o&XDzq~?^SMRC;o~LS z^qpmv>yNx&bghG3>1Zc^4>kW>u4#kS)w3PAK%2Yd@9y|>d*S?v4k?uV!0po|{;y#R zJ^!o&GoQvDYMh7Qhg4O%^FJE(y4j?Z+Gi=7oy%#E&SfP)id1v4VTY{kW&0J zKC_>}dRmO{sgeea7rU*qQt|%J*gW!j#btSc*}bhxDaSwSz_=e`-^iX=bj80n)VlM( zaG3Z#ohj?F{+rJ^^TEEmx@SuA&p7gX9V$xkt_Xp(!?FC-*Tmr;>j`9q+j+ani2nAY zq;Su=k+S^Twlo{_Jgx8GK)XsrPVIBA|K|)5xmz?1&p|ECNgD6AUr2fWdHjaQJ z;Q2e&ANwBm=V<)qMVc}HTZ#H;EatB4duZ^PR{S&GJm)|9J2)4*5N*WIyPiW@kG{AY zPuwS{N8g|`;rjhJ>JNVR%NVRV31Yp0TV6Z)E$#S^#Ui6S(WJ+TK3E=ESI6_Lw($9i z)jK2Y$k;To-`^bJ@FhsY`|{4#!pFZyG-{P)6vG+;h_q66e7I)rfmiHLm>5B1} z55Zl@WcPMoY#Oc?CjAEMNETtfAMJ~`ur@E}p~${Gw>l#>ttQ_Cq8}6l0zJe_eq5R~r6NU+CrUDgxxcP_we@#zivnGdnQ;Gpwf!|C-tV2y0Ma{ExJH zga0O@1KQ|7Be>`t6#x7jJa4(-UmN~Qg5`2wVpk^Zv=?-hMXn@rcoZK>26x2iW-Q@QM7e zA7Fo!jIob&dgei3tcBe>^$z*s_P{iw1HRe7>x{7zcirO1V}Ar5A2^SYPPXYsSueC< z`G9UbcaW4FU!MgMdN6AYc$M z2p9wm0tNwtfI+|@U=T0}7z7Lg1_6VBLBJqj5HJWB1PlT;0`?Z^)cL$=7tON9bE%r6 z@je_s8jat^@h#DK>oJfy(KwD5pmwL~AR2S~)(1G_H=@ZcYarea { 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 046/224] 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 047/224] 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 048/224] 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 049/224] 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 050/224] 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 051/224] 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 052/224] 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 053/224] 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 054/224] 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 055/224] 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 056/224] 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 057/224] 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 058/224] 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 059/224] 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 060/224] 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 061/224] 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 062/224] 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 063/224] 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 064/224] 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 065/224] 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 066/224] 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 067/224] 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 068/224] 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 beffe44cdb43b6d30218adc881800fb3c9238e98 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 9 Jul 2022 11:27:59 +0800 Subject: [PATCH 069/224] 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 070/224] 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 071/224] 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 072/224] 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 073/224] 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 074/224] 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 6d61987c58ea0518dfe00e79fd8db0abc571bbc7 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 12 Jul 2022 11:51:58 +0800 Subject: [PATCH 075/224] 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 19c3c6034e95e707825384f470a9a8e9bc7f7d34 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 14 Jul 2022 12:32:01 +0800 Subject: [PATCH 076/224] 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 077/224] 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 b1382c2d5709a5855ed535b03a5b32067075f215 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 15 Jul 2022 17:00:37 +0800 Subject: [PATCH 078/224] 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 079/224] 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 5946f6e47dda9c2df58323c52e48a83533ea3e80 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 22 Jul 2022 23:12:31 +0800 Subject: [PATCH 080/224] 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 4cfa84082223df106e0b137dba86be93fa140b6b Mon Sep 17 00:00:00 2001 From: kingtous Date: Mon, 25 Jul 2022 16:23:45 +0800 Subject: [PATCH 081/224] 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 082/224] 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 d0e55f6f814f066297dd51900794962630c95511 Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 26 Jul 2022 17:03:19 +0800 Subject: [PATCH 083/224] 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 084/224] 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 085/224] 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 0ba8b4079b90924ec82b46acc6396d3b7e901d94 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 27 Jul 2022 22:56:28 +0800 Subject: [PATCH 086/224] 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 087/224] 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 088/224] 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 089/224] 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 090/224] 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 c4451b3cc7d8e77ad8ff9de95bdca713c0cf0434 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 1 Aug 2022 14:33:08 +0800 Subject: [PATCH 091/224] 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 092/224] 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 74a2929bc9241599266fa6bcaad1c0d523c324ab Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 2 Aug 2022 13:10:09 +0800 Subject: [PATCH 093/224] 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 ffbab698b790dff3ea354588466c8f33de400f05 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 1 Aug 2022 20:42:30 +0800 Subject: [PATCH 094/224] 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: Wed, 3 Aug 2022 15:31:19 +0800 Subject: [PATCH 095/224] 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 096/224] 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 097/224] 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 098/224] 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 099/224] 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 1977ee951e8ff1064b453dc33b386e588f92170f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 5 Aug 2022 10:27:06 +0800 Subject: [PATCH 100/224] 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 101/224] 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 102/224] 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 103/224] 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 104/224] 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 511f3c022f6598874b7e1694131e6eca4e7c0e36 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 6 Aug 2022 18:48:07 +0800 Subject: [PATCH 105/224] 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 106/224] 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 107/224] 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 073e087a4810922ef32aaf10471cb294f8cb886d Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 6 Aug 2022 17:08:48 +0800 Subject: [PATCH 108/224] 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 109/224] 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 c5d062829191809978a5880af10c3f6d729ff7b5 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 8 Aug 2022 17:53:51 +0800 Subject: [PATCH 110/224] 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 e420178750574de079ee604e1e5ff9f46dad1cfe Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 8 Aug 2022 22:27:27 +0800 Subject: [PATCH 111/224] 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 112/224] 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 113/224] 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 114/224] 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 115/224] 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 116/224] 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 117/224] 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 118/224] 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 119/224] 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 120/224] 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 121/224] 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 ec3f7a8e91b24030bb4130ab2a104f007965b8c3 Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 9 Aug 2022 19:32:19 +0800 Subject: [PATCH 122/224] 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 123/224] 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 124/224] 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 125/224] 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 126/224] 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 127/224] 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 128/224] 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 09c80bc585d7138eac86051d71f3938aa013c180 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 11 Aug 2022 10:19:12 +0800 Subject: [PATCH 129/224] 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 130/224] 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 131/224] 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 132/224] 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 133/224] 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 327a712c363c3ea545d622b5366fe2abaadbbca0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 11 Aug 2022 21:29:43 +0800 Subject: [PATCH 134/224] 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 e6329dc7eb70766ac8bd6aaa25c2493be03d2c65 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 12 Aug 2022 18:42:02 +0800 Subject: [PATCH 135/224] 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 4e4f83716029778b619cb418594b4c3280d8e784 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 11 Aug 2022 00:12:47 +0800 Subject: [PATCH 136/224] 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 137/224] 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 138/224] 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 139/224] 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 140/224] 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 141/224] 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 142/224] 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 143/224] 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 144/224] 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 5887334c2e5118bbcfabe45ea093ca0ca70a99cb Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 13 Aug 2022 12:43:35 +0800 Subject: [PATCH 145/224] 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 146/224] 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 147/224] 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 148/224] 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 149/224] 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 150/224] 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 151/224] 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 152/224] 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 153/224] 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 d9c93655204665b6da79b182de6170aa0a88e4da Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 11:46:51 +0800 Subject: [PATCH 154/224] 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 155/224] 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 156/224] 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 157/224] 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 158/224] 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 82b72e5fdde6ce6a8190baee7b7cec2ff335d04c Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 16 Aug 2022 20:48:36 +0800 Subject: [PATCH 159/224] 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 160/224] 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 161/224] 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 162/224] 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 163/224] 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 164/224] 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 165/224] 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 166/224] 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 167/224] 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 168/224] 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 169/224] 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 170/224] 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 171/224] 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 172/224] 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 173/224] 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 174/224] 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 175/224] 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 f4d94498c0c0b0e5e69ae5893b0b6cfe12041786 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 19 Aug 2022 14:22:48 +0800 Subject: [PATCH 176/224] 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 4faf0a3d35874cce32283ab71566ce07b47a98a0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 19 Aug 2022 15:44:19 +0800 Subject: [PATCH 177/224] 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 178/224] 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 179/224] 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 180/224] 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 181/224] 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 182/224] 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 183/224] 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 184/224] 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 185/224] 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 186/224] 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 187/224] 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 188/224] 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 189/224] 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 190/224] 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 191/224] 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 192/224] 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 193/224] 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 194/224] 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 195/224] 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 196/224] 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 197/224] 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 198/224] 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 199/224] 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 200/224] 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 201/224] 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 202/224] 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 203/224] 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 204/224] 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 205/224] 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 206/224] 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 207/224] 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 208/224] 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 209/224] 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 210/224] 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 211/224] 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 212/224] 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 bb64690ac97ca3360c3c134718f95f53c6257a8d Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 24 Aug 2022 11:01:58 +0800 Subject: [PATCH 213/224] 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 214/224] 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 215/224] 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 216/224] 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 zcmZ{}Wl$VU&@H?yuq^KGwgh)~*9Ui(;O-8KL+}K54H7K4vq-Su9)i0|fZ%ZReqY@m z_g3AS>h7x3H9h@n`pld-byazE6cQ8w0D!KjAfx#&LjT9WpnrZ)%;F~iK#U)tt?#92 z;Y;P_;c8>=WKHGe?`BP9?PqTT0Qjx$=Nfp>@%u(?en(-4yJ;Xxz}^VUyu74}*Y^E# z*2l;=fa&syr7>D%3&R#b)%1SYm;A{7mY&iRSGBO_(fSh4%D4GgzItfZC7z#nfB1U; z7bo=nw%b3n|3-U@QzSIwEgjTjs$MTB*yQML;5TB##o43vtAIkm{qzl6 zFWbW38)cW47lDQC!jZ>-UGnkMuHJQ)9@8Dyn_b-RSmo*M8do7mglDhBh1ijYT{roU z#}T{1Y-!IbJR)W4rnJ;l;{RQq&#qF!-)g07hH&@Ey}!1Zg-Jx_L+mPQ24JlT|c|P?r-8`%bmS*wxO{*X6-6uH~ zA3UGNx)frC>$}TIx7zCUp3X7vq^myndkq?Ao44(D3@KdHucGizhuzat6cTuRtyx9{ zW8bTy4I^@yH=&MM+s5Dj5ZuvMRJYv|lMx?rgH za8i$Iq+AS-=$MCPUeoleiVLJZF_z~kRYO`9JV!Yz@;t|Y9i~!o^y$V*RpsfX(uur_ z#>%xViyJ|x=H?C^>AFUaJ%RHb8i$7Y_r3+hA(7bfB>rg{@}$9b#zUf@6IPcj6Q4jhbBJj&iB%_`EEOk422#i_7#1$=jn@>W}Blqf!33oD#yY5 zqK+btO8#AuxOsH@2?>u!qkbBo4|er=aFLt=)~bR{lk<3E+@ob{S^H_6nnZ0k$L;rP zujw7)Fy`ixRz|GTzI!r&M3sUmp-*O-R*Z-u23T=Cke0m#gJI zx9IqgnfD{C+2x0&TC!vEBA9Y7PYE-oQsq()bKi}(9a=H%e!Kc>YJY$Dm-UJB$aC>X zr_!ty<+s00o}!MMKNrZSO>4Vkv&iey_`bN?FF(5*$=99cHC&G5DtpQI)-PtsP23yI z`9qfX@5wC%FMT=h`I@4+xqf6Q`U=*b|9OrJ%8Pk)q(N&eoG?pl!yeo{A0ZaO#q#Nc zNU++YOxl6gbUgiX`^oz(`_V!u-UHLbRAJpVSMv|q@A+YSW9*(=9l}wVxE%N8?1xu} ziX3Lv+g<-}zv&!R|Jj@;#zP7IN$=@OpwkB8(hcLaZ z2Om(OBe{g@-$%*Y4l*n%Gb)sDXS8pRQp=|AAolcJ3u)=m!pXXZ>#FGKD`6`>TV|I& zT9VQ=CwC?HN31XUcr+&WALjsyeZHnu!S7`_^2CgtgrFKcc+7ltclnTn-DNM^ebE{8 zA_MVw@Feg@YHZfQxUJs>-~HMQ9oV4ffb@pLc{T2<$V;0%iZZ8>%%*Sh!(A|685B1T zuGUfP!#aJL<>zAmK%PFa>ycz^ujwCkUqB>nM5wP(BE%*&&&@rmXXphE=ngJ$Tq>wh zd9=4}^2lE#I<}Fi7Xfh@-3LzIT)Z_HpI>fe#k6U$oW6VB5*fvLHXn3MeRRN`i>-4V z=x_#`UPro}wFV8`*1aTKNk?hubQC9un)PqN@3Q|TNqfK5=7z9%KMik?Bu7#b6`NNX z3;R*zOgk`ElN77MU7QnH9VY@++v;YyZsYINH$0rzpsD!>6V#gb>>cBn$^QEptdv>d_l8jB~}MNo@j;;E9@7|&Xl^g8&i(UYf2kJoVoLxnnXli7D==^2TMb`)AfLj@kl+ zl~RtqbG+1eO3KyRI}q_6zE6EfP+nIPrzQj}?a}T>{JqEzs3MKHh7G}ZhU?2_>44Bd zo`oq|A(!(#IH|Qx;=w7Wb z`yy%eCbZmO*tph1PYsp2Vz;eig1`T&-7}9p9v*`_>5@~LF|dmNKQ|IGF514k# zrk?nDjCn?*(txQ|lRVg3YYkSIy^TdBGDEq32afwMdU7Ys!vE;mR?6HL>UTaL@~t8ju8>W(B+b@2s-*$FvK6l4sF)%VDU+rrPyBxHjNA){z+(^x7xR;>l5$l>TruR^GgMFhQ2MULTuEpW5x!Pjy?g}Aq2|KdN2fkvFx4?4 z+B*3zvvLgMDN7XQ;bCcKcux0Y7z#7?}!xjh{U}M+OMN0aHh|hpH;lUgpBC z+;PqzCaE@61%Uh+gut9|^jC_8Vko~57ty5cl9-A^@VZrc>&ffr#e_De*1yHH>qGU4 z(QS?zzK&G9B#JxXXV+P7s$r$QO_G6l1Q~tXAbp@vu_pt*ffI}MTv`4e2A3s#91&DY z+hH36DES89QxeE+FPQRl#vTawf!%xEI@bALmVW{jT7-_qs?%6k|3Kx?X7ZosEPJ5} zLPIGy3ft|9S4g3>lL^2$`q@qo>U(rRq4-j^Qv(`n7(YnVIMt@rx&V>$TX(eP?9#J7 z8v8N4sZ5ueyH1NA;Hekwj_LVR4Yr*=$A>Z*Fn6urd1J$u_`nE@pd~|SmPQ$yDND^! z7We%9+~h5Rq5PSD`tU|wiH??YNBzf6QN!Bq@$XvTLT745L#BkXEuvVfATJy9MGWzx z5&@r)_v8de5=SqwkX%qQ0kfRdmX%qcXUTbr$1OX< z+_~VGn}$d%R@&r#Z6Z31WdBv=Yv{O?>(9R7Aqm|^O9#UPg$W@XK%6D)f$U0X8Sx!} z9oB!M#m`J0W&?{oU~=AlB=r#a;!xDQ#x#JG!>tBRBB5T<>YEJpT_r|!oF zMFU6{H9MR*1bOhKjp;{+puy<_aS|pKS`xq<#r_JltBz&{l_+NU6zv!tKzeCfGIxWR zi4ye;d_T&GzZZ>7NIHNUo2yx8B5SxkbbWeFZBLBJ?YL{$>|iZpt4OS#Mc z)*WccNcMI=zA&%*tCw%tu-fRYhJQl9JZ?%PWHMn!>cEpe%2Un7dkBe~q?A^lVJ*;>3tJCAHB+daYkx`8M- z-+mzrkqo3Z|K|_1<*_WRSd!L$*?t=l=E@iCyrQ~^dv)sWiH@$2uwJ!cp}=z!))v3) zmn0CvPK0B|sYFsFI5O_jcXn4HCAR*rSPs!b-Xr5H8H34g!~x?^Mgm?`Q&^&soprn0 zYy*RsL9g96zpF+YAcofm1$YZ7E_hG5m|L3k`g8PpN7x;+#O!bOD{6R<5zYoi&ZXrc zkL=22i$`qDm(J-Ty;OZTCS?(>|I)?;GH+00{9%$CD7VGMbEt#61uC!P~N?ImdnI6kq zYhH1CtWCpDJ$tMvkKT(q#EfQwGL1va+Km~%TRKpM;idK@BKViVlpwmS;)N^bT4^Fx zlW4WHK4cgYz7z1XEz7+ZJI!z~8JR@gmBBx1e8P=#Tk^!fLQO7p{?E+tSG zO`>?o7puSKSz!1uk|EKA#UqCtZzUU6p6*qyKQNY$R$Ng;&l5C%l7P`7jlXCcQ66E# z`ZlIHzU(m)%L#eq#1y4>wW19W13>#Y&ApLN>!!5G{dRnp5f%2dso@e~MUzx6?IKK&u_+ z5&)6wdIwt4Ib0%Mh2;_e8!s#>Wvgg!`?6Gol`OC%1Gt37WI61gmNb((5O4L&(nQf? zuIGS2KCF3kk*qpeFjY{7()4IonJfm|lFhV0drSX^BY5v->=WI=M00`VMU)6#_tHjN zpt`mM3oLRE$$1da0+;IN-qKvOV1XMnj#KvFM@QzM?Pw68yRZbz<{Lz1Gsy%(9b(Id zWQZ~Lzn0Q+7EWBE!VfoCMmK99iXC9LSbdf!Lixkrflg5P$5{#OtQHd1n;BLG3Q3y|q%nvO5yO`cEXS2yIL?BPuDoqrf0r$~wE2BxIaP z4bi{yD|$F)r-WxIK;O;`$CCHkEz1Z{#&vfcr^oa8pp(ZZvK@+k2f2qMU<bkMc6~;X?ui;#7-}pzX}#v2l(L zt&!e0IP!BM`BEq5zz1CU;ZX%{#Cmvi;4dHKO+x>O7fg+j^BsG!h{gY&>zRBj2wo;Q7)g>n>bpO475;mkilO)+LM$wGZ-0OG zj-2>c>h@}AD8a5fUUS@1U3XiHow5h8R-f`?>r&$ugveZ#^)#Dj)=wQI@ykNTImL%{ zwLep5go*mP+Jx8-V~0_vO9*dZ%HywFf-0*_H*X+4?I*wMt>4?s|aegM;9Lx=~!66Bz*)Toes6jQ_jbyF=-WDqEZM>*4^;`Ui;g# zyq+~L*piQ0>~$_oZW32%FWFI>c;-k&mzWk)u_(%7x9s?B@B1kc=EXQ(@WRwIp2eU` z^SdVnDri&nnGumVlM(njen$!wfOvv=@LGqCwhEO!J-ane3!ntbp0Ge5r~?M@4J~Q)<)Hp&=$SZ+(rYLc@FT&hc`|qUeH~`tb-AP*;Tf)CNU)$7Wt|hC^bE1(qgl z1fja^-_CRIEBEHi#v0uJA`$Si+o5rZqlSUQ_q43u3Wzx+oh7DOgLr;;Z5?s|6KkTv ze2y{soJ7mvRGnt}f+jhh+|8SA$m}VmeC9-^ZPwd^#xOZAj&z1^nX{Z}y1c@2#KY!`faa+-e4EQudeo6?H}SD4$|XAuA+QD!YvpLEMseVZ zrM;k}EkaecWHMWQ3!jS4!e9KkoDkmyE(hULeY7HTZ-nos2}P`v&0U#^I!uZ`E$*qB z1z;U@>qaU5&28M-m8*e}8^OQ7)G-&#a*VSWre- z-xt7*0q9K?I8oRNt4G!cX97X;e&bK+iF+_?z`$9N*PLK=8XpN#sZLCig#2hEp?9;? zw{Nl0$LtSkdWU&`j4KVnWb3R|63sWCyV3-r-~{JN|609(`sSDM`Df@_*ki`Dj4nbG^6kob!=yqlG{bXyW=3B%!DsTu?Kv0OWQz7nM8HLqSRN!KyD=@js z7pl(6HR(W##Iw$a`@J$tss2-#o>vj2$(5CgO$H}}qI)x$+5Oe^9os;YVd1mqZ6$F$ zoLH(~2JzSbx+O(=>`C+P0*Zay9= z@@jJH6=r8hoH0rViusn>1dYDx`cBBpYK(A`Y6TxJU$=OxU&A!tw5H_@-l$;|5-&n z6<_`#$ZiS-o&W#}-v1cfoukp-KM=`F@jqFlJrF)Jf)E0K^gjg6R#8S$+i&$e-``DJ zmSRNQ6+##-jO?5891R%92WJ{Lj*oA%)@{+8w%}NBriFhp%#b1?u)t4ST-VW7vOM;v ztW;><>$QYp$g zp+rd)|DU0<)h=VHpoSj-1P>pEP{gWE7^`k##%K-m-}=x%sem-+Zr|}2N1|~9JqsP} zhQye93UN!>oQ>_gXbq^abPAjXa;L4cSWsWlu$^}Y{ey6!=@P=$49a7m;oUim(SQH0 zJ(vv`l@({WSSVq$?ah}F>;S7hSM_1cPhimVDKd|Eq8Z`JcH#wo7z66juA0i$RTG?N z+I57I@TC(om;#n{a<0dh5?p480?Byw80H5^ zlpKH1)IiEFgY^KtK-FTIXjbYV7SK^$fUyt`kP*sP-B+H615yDv#HfiyI^jSbtUN9c zGbJ^xh&G9LNTqZ!CRRPrJ5D)T;S!F~T`=OIRE3Skg`xS$*(~ zkWxxWdB_dr25#Xv+DW})kw$b1j{c2<(I=(1e)K*O%6s=fa zORpOgrnNW&RGKw8?9&E=t<{|tPdcb^h9Ltp7gu*K3I?bnO2NG*Wy2N~!>acGjz;|= zue1ID;4NWFhRczQZ>g9<=MY0~oeDn;T%Cs(IODpVAMW3*kP52tlc2!Y=m1!jyf~T~ zJ7)!p2i-8VKZ581S{+r8k0RFXFCWCs^m~Ca+?IkdAZgsKn+gjl?tkg5HIk+OVx}f* z^Z%p;PB?n*K3!exUU|4WVdE(pQsoc9fGEUvF@W#erfq z;ZWTbpB8SIu+!@?y+4zoe)UjAe-i>_B&PhvRbYT38IqJT2+h58vJ>dN4gH=n;CBx8 zqbtKm(?ifi-YS4J*l{f+arL>7KU}EqzaR9XRZJNz^pl7Pf^6>8f$dEEaxiAxV$zmmYSmsX@S?>{jjOlZG=Gy&^KEJaS9nGTMj+r>29Mi-G~$HA`|k8 z4NoiPfXwFcW$41v=&#dS2oKN;Y zu6Ty`qM8g1tdl%nztS{2+D{}@_W%NTphRJg^lTYYCj@(twG|J7V(ED-Y}kV?7P0A_ z$1g#*GK6FQz!?3GBZZ?mDg=a??y8lY>9^gL@C45kBf2e-fvqJ7H5IfK$aH_?wihXi zka8LAi3pUNDKb#uC4_=xma78<+mMzDhWPdfPHfxO1%$EdqkNv^hv!GpY6p+}dU%!%GOOcbD*%=V2bJih=@U zVjF*vLvqZM18yDI0GO7ff~pKy_LOh}h$fHwfBr$_?Q|(BRrWK(a+@?&^jSit`Zz#} zB7LgVxnt;T;yEpp6p_#igFoG;y{LK4$PT0dvZcEHjF{^cHL92P?{ zp&tY;pp^hE;l^A|vJwo?Z>B^+AQa76iji##J4hXQ$fw02q0z5y^*~ zfL**!TqSi6{*HvhwVPT6n@J?4mAQQyD{Y0_x$A-YQ!Gc8CuVa!_S(7iB)YOom_`WM z$*2(WrEN?RAcG=WMI1*ocPVC?5?&sw?UNZXAe3Ui5S$sBcr99@jtj83S`vEy3;WF* z)ssw|DPRvr@u(crf9pQ{8d~zP07{RO=Yg>bJk2#|p#9k=)Y%HXo8O)SI1~Jc>d*XQ zK3eAzo66@HH6R zNmu0}+4o^?8!D|PEsVSVkZrkAh<;3b^@rk8%VAl3^?7)Jwx?Ll_K3)}&dT2rUF0cI zcB%PP=leZx)V^9k-?7z!qa1&or%?~NO)wlo{dH&d9L5_Lu#<=XYpzYx4fxi7et=Z< z%tvF-wLY9IE7OK0A?sv=XE$IuPTJxIjY$9Z{Re#xtc|>XVyA)C3+^MwfoMlvTdEwu zj>8rysPk86q1`OtYx31LqsY~k+o{J5yJ=YV<-oCKnKYB=Mn35DUSBV?RG(>*k;tER zNXF>n3yy+6rtSd^y(Y4RFN{(T77f3&KzU4S;s}*B7%AVs8bHziY`x9^tWF02ltegg zP4xSgUpj+>1-PcdlSK(k!ij%U^TJo9axZE|q+SkIPj2zK_ii({d**i2_*P?4m;&RQ zQr;xWnmbxMF??$yAMM@x%e1(AA&Q48BI!-%9B8?i_7&d|H5rTo2dJ!3MVeP0NMd#N za@?2>H5pl+*l*21F-m7Q^zKGMB2V+ib~i#VhotXycI?-j=s)jvFG4XJ_OV2ou?n7+ zLJCvber%tb3E%be&Tx|xs6dC3hR+RkwmlU0FZZ;7z^kJxD+(49_$qO~-*$tjSOaHE zTcU*KSHf<3o>?N`C!c7cW&a)}NsE2CRU`Afsg45H4HV2XVNjtnznE&d;5q-vB$fm* z7Iw3)u*JTjeg}{wdJq+!stT#u{^*%T$t1>$vLklidbs%pQ*5qleBG^Xr5)>O(bYn) zc=^bKxQ`_T_yM@{(eI?&!_N!;3jg*~RF|-1MaB#Zf#nHtxPUik>Fo9b?J3Jg)y$d& z@&3}1Oi?@))ArBy)^r0SshH5gIjjGA_sq5C>^f{xA~f41H`-lY`ey2;-nT3|@Td0C zpSq1lb8a?PM)fA>s}GoLz0+c}y8YOLeEiHd2pSxg2l`r?^56Aw7%p^Ju1No`lLs#j zLx~fPWk!|MB2x;d*RnW+Pymi-Lg$qYeV`SK5UMN;F01av6sr2iK4GXZu!t$JF%3Oj zj(~*yQ7bGIs9%W>=Lw}WztaJn!vYXInGIaVyD#rciJn=HkDUJX{H;^&|DyH(KgrjyenW~K 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( zMT5X*D7+qWtAH{(APd9|`G1l}BFM{r2Ee54k9dN72Gn`iJI#n8#ixv}4 zAU9#PU4a!SZ+HiOARo=E&QAKksik1$9K-{NprX>uWM}2t!Jjb)1%4BOs>mNofW)#9 zqk{klh&lmPxWrw<Ie#OjF@a{&c(1KY9P^y>MAxwxkr7c-|JM#u0S*6dJBz z<2D<@4kh~NTZw=8M)PycPi1trwgxcz9?gVEY^*KHEzLfkF$S2;fA~+wY>Bxg@wn;h ztm6P@{*S{x!(CRdswcl|`#J`|XxCJihoyQPza*Wh?s$~yd!IGt(2#vkn)xaH(xzpc zXu0?nY|)kYL_VyPH0mp4ToeI@W^Rp)oH4%tk^F|og97U}->l}~a+%K@lq9NAZ@qKL z^JR-3mEn;*a|&;`EN-Y_JkRfGNM=K^eZ%~aFP+*jbhMDoWY1LoO+^=K9SB@CewT3}}&DYuCF!H%bFy4TgL*zP*9ZTn_nnE0uoHIFJGVR48 z8w(mV6zxfsPix`3u?(vQy|+5{weL$wJRAR2IevPc#exzTgwax*!%}dHzICYyn)o5^ zZC>2{F+D=3Aj7y~02?PHxR3k@pw5EQj+B`rQ0NWsv1g?uwz>?5X7TVHwr~DPK~yYb z&~xS{XukbM&2<7lh$X#u7#C;nFYI&TrK9SU!F|x==?;>UZ3;1FNog_uw>W3yA#B3( z$VXdw?cfvf0U&PyqcO>O-rG_&6*QpUySmAhU3od-EC^~+Gsj(fcDWUEt?eJs{|A{y7{cP)VoD*GZBNB{M^WHAP(jQV!Tdq>08M)Bt1M% zeWF$$Uz=cr%aky{Yi_J?{P~oPBAcT_%Lc{Qx5ia`Y@ESY`yy+3Lg|4IWaRM158#O> zS>H%lanzN+lkZg#?0THE1;miwA&j4~OtE}{%{OvUgY$lOV4fm%>`Q2%Gx6~QBP_`e z+w;BgFaVD&(d4-t8AWW9%;zOtflR#4+cVg8W55)+GI|XkubBE!pIY=+OE<--^@9!VngOH6S$f3p=q$*ist59a9LOZDRZeku6&r&$N@Sqm?J*b)pM z0eE&5RMSUdM2n(p+7!(ArWA~bqNM$Wr;G)T4+9>4V9oXY4Q-ze>37FB3gA*bl$}{r zGORx&oaKNLcC#>T^xCXwxty2}!2>)Rt0_D*5JuoY1a=Gy0*}Q~Jv_jSoBkOm>K0&X z6hM76sI8-TZI1Qoj0!J`9-?%&$Hf`|$oPEse`V;&__V@`r8 zf;C<^wnx^aP8Keu8P7=>nS7(tCFdbBT_-;=g%DHY8#pzrhBlT-6VMs5ZLTK=s zQw&To3LKfHT*n^oi=rJDU;8Q(0sGH6m#um$ORj;G#b!N4PMae!in;Jc;;z6_9B}A4 zK)SC7h$I@UYvCu!6+wWd_hPt~uU8YXl#_bOS89H~WL2LuQ7uQi!dEkvQU;w`0{Jjj#J}VmLY|ZJ11!53iK!qBxeg+rxW{oEYWv7Uym-< z6k^&{UtF9(k)~3lhC}AH-)iI$pF4M!e<5X-&>9^Pi>})PpQZhV`>7+5pLwfP@&#+S z-DVoiZEtzU3jTM$QS(j-jX0gnzLfSo3WmArcc#8gi*wSBXRK;^n|mivyY{Pssf(II z&WWCjB2{)omtD@BiPrO_;&HD%O>_k6U8_-!m1Uv5OsD18GlbmV?hD$_&Eaj!EBK(5 z(G&yz8JMWhNQ&GU|-2;zKX)b4JORH zgo<)sPr32}PgQOqRIb0>luDsp4q_Z%Q&W|b;i<@1egfXGZYnD$+)a#lxymvoZ}PNX zq|;Mg{-U*Z$+TO<{_Fhn_4l$7<1A%6MH4%}L@*J^uUw|9g+2Ra5o871TEUva%Kfs2 zG6j04q8U=r#&M1=!YmPN$Rbv*e8n=+5vl?WszCzkc6uY?%*zs1?wroY%bLh~^ggql z>V(p*w>l3;5lphViL#uCw6|P1USdS5F5l6=LYa z_}Rda@at6yVXw=C=#DmGUh6glUH*rYuv^_B>KR+froGLJh{<@hGSx4%X+`dzBCPC3igOSx^aFnhs>sfx1C2f zwGEobI?UX2Q0*Dr?v#li328VK>;Ba3jzHQmOTNvISJ>z_&KGZeDcU=#%Et7g@Q3tu zxu_rO8yZbDX~XEh5)5hWi2k)Vje`e#Z;3kKyorx%*sE@ky|AhL8ji?We70239+xVL zM%&#fcFB%yd!^YGPw>wkt5AzO{7F^0CWT#(@ji~NL@9ys69vtWQdvt1=}2M!H~c>B z3}7vM8c4KCDTP(N8&7khhQ{DjfR{%siG?sFMH;;B$cb07-@)TfqnUtMw<)edl5W-# zwa=wL%HRHiEJr66MB`xLb~Bc~oEr8SH;!(`Zg;rzXgCD_t=wGrws0%Fq#|Pdn$l{G zFZz52@Va3(8l8I9>Z`jN2Ys5v)R&Gp5l3kq5@GeV;!nu&310G;F@1l5D$El5bZ#zL zpDZ8VgXpmvR8LK;Jr?bGFTf1dleKU+Cy4Q5A>RLinwq|se)dc$r6@=l_vlGp#=6K$ z;c>FQ_tjuMhRq1^{ar!SqtN8DW=+m(uR?IpM@>FI_qXO|p9%dbmlk!8+Kqia^lsLj z+lZQI_fr!e8bCbS5wn@}v)M>^vJ0=B@~hp&ZCwtu}!bp#k;9LA&()|`_ys;c>6L927!K^I6u52;MA0DQ{Hjfq}^xo^l5AK;& z?!xbFlnbARxrgEhk+hRKuSIm@n?LcW+My1^A+0~4g0RF-g^^^H2}d6qJ&Y*u0cD|5 zF8}1-m0lTV%&psDvV-L>)-ZdP%~kb9kwl3NYjk`NEk_4x53GVJ8c(BH$aS4IO-6gntun^oGLeQWgAwY1qV8JfQ zZ=bz?``z=-eP@h!|2r@ky;jvXzd7rxIcu#NMMtQs%3-0CqXPf{ECqRKO#lG+cnJic zB0qi@x|CT00F>iC+PZFm(4 z>oBP{ov{_HuFe?c$frvz^mZaIn=>t1Jvg!~`Nf;h9`0{iAAYEv3$`TNv2+klM6ZEy39SkzT`M+%XOl!8GQHVt{`@z6!~aNO!W9J8uIX~(=KvF&A&3>z=L&at{^C?YcblIJ(YQk`D<_Rg<}!nQFr# zQI&K&G7qU4SsuMSf8*YnLF1Q@b?`RVw`x*~fn`2~(1Ua}^vw9BE+*#y0ZR=hJAUzH z#_YlxovVe$tJOi?Nt7Q_}^V`UVlfAd3MxY%nAE&oOqYXaoKfuDt7I1 z$sWRZbwR)~Nc5rwQ!0JB%TPwL_iml73*hg6$5Zv7Wfp_ENwPTz3d?=flqPtqVQ_J3 zZ}`^xZZ~pe_x>9k_hf(6BrpN^DsV$Ru!eIEH{6_FA}C=R$2l<|-t`+M*V7O9Mlo!= zUwlhKJsn2hOLt)LC4}1#Ft_aR$?GVl|0*xnkfJwG72Ha+b7a31j!e;ZVJapL_*SN) zHP$erZ*btwV^_YRqwi34&gZj_jJ#>=b5M0DD(e=6f_2E->~}mKJ#CrcovLS*`6gE8 zy0ZS1`CWD6FY@P(U%UI++YTFVsGl_>YG%2eGL`0d{33g+-{vtr*7VKi>w)L3#X-`n z|Mf4NI$}}1C=RXG>U)bAPot{kCe*;B%B+OM6EX~UYde_*QJG<|G2@Pz`1{~(EjLOn{ z3U99N$Uc@=4>1*IPg;i>yP$BIDMKw3t2#s*rVf5euT_p$1_gGE#MsDMW@&z%eenTz zkLKl8pR%Nlv&&XD<#fjTXqQgW^v`;}qe8kmQd>!~EY%r?3-5A2+@2KU$5OmxQ;x-) z;L}0gs}}2jIKcv z7@|jX+uEmhORCmMtR(_X)Hq@t73q>~!uDjadvl(q`U48-;k}cs3ZBy3?2F!vcK>*A z!1l7Uikr)*`Jw-lg{l*`(Hb>(RzefPnwBi&Ellw6+wX7qMK#S_kt|}R-9qUT zBfXB~iARtTfjRfyuHAf&Si+d@Jxa8@19t9Z2?!y7@l&{gZ2sg38>cek^YemV`HN2q zR|eMjzdQ+HaNQzk;Vf(yoRw^Sne}|8#j}|EALm80a`p=Y(0dXF3Dvk2U0!b<>a41tOBa zZDVT{-MF3;B-F~55MmfTrNw>oyh^!5k%@_{qRJtCc>?;@PD+q}yuXYJ&!LP6?9$l5 zZVpY2?EhlrD3Sb2)fC0NrvWQfMdJHna@(o=$MNRu;S~H&VXVauuFfU<_k}oRqQkv| z?EQY$>EXC`OD=Y?_Nur;)1n7+S^*j2GSPUN66DYjtMVBS2HnCJvdid5P{Agz-qqr` z?4?iv^|Ou=$n%gDmV7$@*9C*2%pY)s80TfGPe*OCyct!*g8@i>KYAzMAqrrNIv`q z{tuF*-7dLuju%8^OschL8#pYSZ1H!PjEw6I;Gq`oSuYBsdU-wvzH|#AJ!Nm-pDR($J3AZ@?GaXf&SsGe zE~m-pguDQIc>W3*p8Ukb7c;Wh460)9n%azEK{s`^n@SUncP%j3r^C?MEM*&swmJ5k zh1 zKT@ZYpccSt$r~NS{Egt%eg&8dKg$tl$>q3n3FD zyveJpDOB%81lNrFd{ruN32& zNuHzBOA;BDZII1;sidyGU8~_=>_ylIk=RxBUyFr%Ci77@Yr}Jw7C$V>M)z)%yZgwP zhXnF(E~_-cfGdXM>tZzBbH7xPx>D3NxW5mW3sD)I=kNYVLog@ZB)r;>ua?zwey`hB z=;i@kcYl=;5mv&w>mwxp;!GTGFLkM3OFi6eTX(JUIf6PSDVCIo`1Pwa9l9S0c%bKv z-WfzZU_indh_4KjlA&lop8Q+CZesp}^{}s61VC6ONX|mwr4o9w(uTr3g3z8Lp(&RN z64G0vuy5Cd8K51^q3P-mmN1-;{Z;8}E6B;)DH#-6QwVeHFAUx)=8)!-mz;5bQyZI{ zx&zk*%t)o9B}5{7JnKg*41Er%nijDF|rVQ-XY8 z*8nIS1_YoVyviV%+d|KJb{-&Z50^7!mqE{0e)_FOM@%RHiR?u#?qC3}IWcOJLK|ZL zT6KY}0_UDoG{Ia+ASF(L6KZ!AKv{H8!8`a_Wu0nXRKx+0Q^`>qTt~=)2|JhX?tU$l z!rb|N!(KSq;}eD8gNu>fs|~RY`Dd|6o-L*5Z#AVH%@Pg=+HrS~dvlaWK7huhXh#@{ zXEtU3Yz_gIesaLW{5c_=&8c7(VqNJH zRpyVS@(DyJbf8787tW|H+=E9{ei>c$wG)g+HbnRS*PFaq_0H`H(9c4e&*kkwhZEg| z$SEF)*wUt6a-W=`W!9T9$9)K^eBuyawzsQax)l|3RB+lLJN5wak$uijfq_KjGz?oR-j_g{RRBSEW2Z9+(9RhAQ_B z&XNJL=;aFqmV+VRl(Q0*;_4*&`N4;h(H2bVHKo)2d8@=Io;H~=R+3-^o67;*qx>4& z%4xu-wJQN)ATV{KYpZ*DBVRR&Q!5XwtjbL9Q>^qxY8ubv z%u5=k&N?;;XCkAUx^;FI%lh9e##O#fKU76Sk4&W&|F|X(B=Hu#{hlHN>V)*+=?%Cs zx>%z{gfrCC>T&+W4?( z#Zk8Pck@i(zE(kUIl!11nwZml_mg~Q46VNiKChvp+o!zDS-250`k4olh)OIbS{51_K_ha48#l36)@)jaVM)%D_oQyUPxL3xcmb! zoWhKInP%g#E-cMAxj2p#fli$(Su>w}hgX+HQ-Vp{OuaGHU>F&KWPmFR2R(h>Swui- z44!=rwlfM=#3ZM#1Iigur*=(~*oC-L{CL)Xx&5$P3MA4`Lk_)4zO= z?4B&OSy#W4JocR7jun^B+d#)nyf9Uz(<~{2TERKOrcN2eeAsN+7 zsy}S2UJv#YD$qkLLjK&h{8U5qURt8d*WKwzL8v;jXnk7c7_GvX3r2VH)-Qs$ds>Us z+OdoaR0Lc#NY^&;_4Sxi{sI>@^NLvUSpa#*nMjb>WJ~i>y(EtPLYkkX5`hbiYNG8x!gf0UHk7ES<6dIi_O|sjjyX4q3 zk<>>YdM)W_%;jpTI#-k=^zLns1X$%t?$iy zi7EPoe;=s?>k#kWH=pJ$F#Ykiywp=wQi8)|^Ea617MYtTXX~|foKclE zsWi_%(-X#rcL4Qa&6IfKTKG7a-+qb!C3gVV(x~f%*NPSerj+Q8NemWc!jf}!Ptmn8 zM%!{08;d^#r+E9~7I<%Gp-f#RFAg}EP@TB|-l^Gzp8zn#^#apci$zqoG~FU2K8KsH)P zBbo8ZxiVZf+{n0MQP8o=8O}4aazHPxLo*`kKzr+FC;o7;>!@N|)XG=E4u3~$21(5z zkhceOR)$RioNh{%>h(qOxY{JJfid>_KZM6PKgMn+=pxFvCWi#rwX!099#9ovOmQ-u zsweJ~1B4>f+Wbh7RK+NXiIb|~EK238Ai`ZLGg&~miM7q9W*@}yH1i}5VA4;~+>sat zITt4N_DAvTf1T|OqxM0{>f%TXobN9=*(QAp2s-6|pKfJ4V{{O$S+BCUzUBXpQ$`m) zF$^=SuJ&Vn7oOJO=)VdvQKyA(Ro^(r_2V0^0qZfp1??lO_Yj^~Vq|ddE5=Mwe4~`p zvKS+T;rdX8$*6p1)X1r=pDaAVV&hOSrKLGxPr%Q=lDf{gJ2p9AHvM>~I@7~rGGP#D z4PEF+2NFbw<&YB%n0Xh_-_q`F*5{zCu^!}T)GUO1x3BIsm_)$xNxPSKNaEr3XJ@5^ zbDZCw@*DSf>Shb-ubMWz`P{x<7m$UZt>g_?IWGwv+1tgLMqlSjRJjS%b9!R3cH_Nv z)zVoQ>t0!PH|{*A!RRDifMSWug5~f;dI>u+wdpf8Mp4i-n3v>xVH#ZUAXr>$)Yiu~ zhA%NG80}e%joDa+v7Te842l#;{LNz0t;8_fU!xY(^|0h%8|E*g_afCY+9re;sSlVWm0@6OO;mi4#o! zeMCx%4$m%4%{Vc$oRU-KhQ$l%_SU%cE+qVF4U3H7u-ZLDemQUZJxJVE!CLz?Hc^cc zW^)OFo5kp`SBLFW)P*X!%DlzWoeIVLQG)+$G)yRRagLlQeoYP^i;K*dj?a)Gm5Q(Q z+G3c>dL@Nq{cNso9PD#SuTRin7|arof-idWT7{WNg7p_rB4bDr)COwqmb#762^>aoai_TU&_NOoP&uRHHpMq9{E(dbVvRHtwQHjUNag)#NwmmzjasMy}@`drIqSCW!Cc{9854mul)t@=Mn z)7&o8pQO@zjk8dwGS%;Eb^f#Hl~aeu$#6Wk(TFmW85L#E0nw@f3W@BO{<1dJH~uMS zz4Q!d%}g76c;QTOd2a|QWz~93Mn;Tl?0CIwFTLr73Oov^GLSR7n>EXe+DJ4wc5Bqj ztOP*$vDMU>a8z{51Mc-GIA;Pyoi~}zO!=C4E2ot9sX)fmN;msGAdFbx){E(Ai zD||l#OEjkTx7YVy4f4{bM)U&ON#mO-=ZS~5s`v6-4m~YR@^^|Oqpw{pGee?QT?%wf zu$YZp{aK{Ar;(0vE0+ zZ-8C)H#%ZSiV{4{1G5||aS;0N&0nBnYS@cU*S9*@3oZe+_?f71$;`^bqY%ZWQ0m%m z-{P^VVk9q!;zBV$TSg-_)0$bc;wQr$nd@v`BBl}FvQCOvkHGP-?qexteu%S1$LkV9 zgGEt18|**Ox0Z4cZp|5=zEIHg#RO9f-}HEP7u_MmiPjXS%neB+$Qd?{V|V8-T1$K| zTsYOa_TH=&&#cx$BQ%UBoh^?HKer)7O-HT~_6`U?-gk*xd=Qp<9bsTVp)WU(Q~we1 znKeAST6fCFTFXLZhO1gtuxBgavte~k1PkPUj?8b#a4WQ($h;K6TD5X!JtSjc z5~tsp{fcetX09alLSXe(|JLmLXzWcKLu*b{EN85@szrgRmOVlYS%^V!yls@r)|nkV zDz)#a7eHKl&2Dx5dnt)pwG~O9yRjoOKAe6Iaqg!uEG>`^fTE=GKXc?S-E$)gdpw|@?jDmmj3Y|&K}dAyq|NA6;-J^Vv173+uYkmyPceoxs%*R~ zFoDYVd|r%i)bfz?FjHtjRVH!d1uXbdqaV$!9F~$ehk_1%iPUeP$)2S0p|zlB+PLsK z@b4N>4RBHJ66p|vRDQj{6_KvhQVIamOBlry#;l|N zLb9bR!hR%78i z;NO*TjrjG-j|yxH;qk#36c-1*aEKxo=t_&z3>G2_PqvdZw>o%lx}iaaJmsK&(hTeO zFxUkfNd6EMj}cTXpO6IdVau9fRP^sP(yPaidW4j0$3F;PP{CxG0!IXf{d` zSqsk};%k86i?0DJUU@OC*)z2oiY&CpBxD&een>+TQ>d`awI)uf@hQZ5RWv{!K;H48 zE9+x?Ye$r7mvD;O_cz}zQz_}F1mdSwoqh|A!*hok4=If`tcLbyl2VaaV+8=$`r^ixX&{qrmBAJ& zW+-^Tje0pP7FN&PwgW*eL{bIFAq)5p>#~ z{1?I#xi6Oz-t6;$p%i^zC354L*c`+(X+5debMI-VitpXYWv#{|J#=vQPppYVyk^dd zWvs&hFN@G?Y@Jgm%+@PBEZz|hUWBQ*oknrK&1nPGr7kTGR*OgIQ;v62-O8cb1in!( z!RCQ?No)Zb;;dwlySKGBr%q{m4)p2gGl+xJ`Mw~98qL@OCW#mEygd$NEvmkT4e6yc zMbdURbP2SNNY*o&DAm*7S3A1Da#QFW5^KrOR6_&dWiez!eC+@&AX`OZg4pkJUo8Oz zQ|ICF9VRmIqxC#{kh47L_d-3Qp0<)iiDhGv^ z2kqog3L*5S8ktDx%Xw1eA-`(Aq8u(%{mM+Fv#xsixOZu87#Ho>n|}7qp=jVGp#pOg zlW&~;O^j7%7<77xF3i1}TM5T^lc?Co*tICU$y-U)+7VJiSp^*w$a_lt;TAg^FVd;w zwoNE?a`~i$9I9@=dTqUM!^GPBRE*sM-j%mo?I)X0S9q3gri=wj^Z2e8@NEOLFSSkh zr#)SVLF~Y%aLu2?NW1;Cw*}Zv4#`63$Q$se=gictI(yPQ5bx+9n{FhbAHQhz*=m<_ zMLoJ~kI5!6gtB`M&M2kRP4m1s(QJka2mk|8o7Qm_HY8M4^mh84lCeRx`a`sITm){K z(^Kdp7L>Osl-g?qAC)*mE1Q?9Ibmanp{>E%QM_8+SAm3-!0yd9ml? zDSHgZCtROsquIQp+of&+2kiFUdN*F9!39*yMcMN6Vq5zQ-^4nO=WB#$C7=+;N`6s z_pJly2kU_rDDD+fG5UMUlk%fqIEqdO)NtodRln>E!=^Z&S9EaDIX3U2`Q|;;e~Yhw z2s@jc9NCgCY4mRhvUKX9Fb(vyBPx@LHaiQwkPj(WCz`SNCN1ah+`p9#lo^;75b|E= zovI+Mq=XIV$8R!E^NHY*zj&>k@>GB*HulILX^K5V)wv=YwD7Iu%k!SHC8=_$-8}hE zoQ}JU#CnCe^}3)=-a8!P3%+SgVMb??Ab&i%H2hfV49e8 zRf1EMyY0)vk{WZM(1Vl2s-kd~eY`LDG%^WVi2vdkw%xjI42CULh5f9*QFQC#Gis!X3 z_sKv%dq2?>L5vF7`WbF892_Ssg`o!SBET#vRVfKGlYthX#>OHv#fE^b^fWisZC^sA zQl`Yq2s-C`Y}yNoe>Rew23@d9U?fN}99=GcbCs=L?)h=vprWRXI}%_^E-*SZA2Pmn zsBeBCL`wfPDlr>qH9pE7J9vk~wc5s&f2LCWc+44WBPFG-ASLx5C!3GwnOScVMCAL# zDFU}CHn69HqwbxvCFM(O;=(>1iBMUZskm`Y%kksK7k_wIb98%h=~X}l~(Wx z{X9|JvH*!8v6p4Pa_MF`eZN_LND8r~j23+kLu^Hc>b2ycdCD2FRh71B%$h6LKg>kf;lzRI^Ia^istrkZbU8m zwZf{v{NyU)ar$(lcB-n(F>-7|D7&J@UrPw>H9YP} zsZ=fvc|K2b005jko5z!8T@_^^b0-IOQwt|ED7%*f?C~TT01y`Qf|;7zLEXS+P%9fp z5xS$cE;_J{g$UhCUKNN6ObTjkBk$t^)$&o*HuteJ7qp-g6Ga#H5_%+XfV!E2y&UWv zU4^_v=>G5uJzoEA=AZ-rfw^6@)=A5Rc=KO*dY?gdH zylgy{rsiyBf&yG@Jf?g+f)?CdT>NJIf1^-yv3aZnQ~SS1^_$A#k;>Ep0_7Fp7GUEv z72tj(Ff)COh7-bO4&|{h=i=ow<2Qx=q53@!LK5ltmMxCgp^%bMpM@u`m~s zeWWyf%r+YbQ!6M3%+cyk$M1O&dK}H8vZlX_C`{~ME*wUejg|2Lk0LI24j z;o|1$iAcexwWaI74&hr{YS0+N4w2`lE++} zJmx%)xyr_A0p(%if$&4vOwFL2Y~1{h8OhK0m@R?=|IY5}Wa;K<>H?Lpddz{xd48;| zKl2P`{44#K{vGaV4gH;45H3zO&c`K?SDTAVh>J&vmzxD5AOwNX{WaxaVUFLc?0*bd z`1d-bq9XKnQG|c5Q$h;Avr)?(2D7(;y8OFX|B*cZFSx(i|07ZVPv(Dz{beoX1oM8Z zWNSAyPsjh#{eJ@di$U4O9O~%m^k0SkcgSC|{Ow`%81r9kj}N)WXC}u#ADaJ2mEYC$ zfAQxZDfoZU!z1N^;VGhu^<>9i>T+Eod-#Jy!q#gW&fc5Rj2Y`q+r#rl2B& zvVlp4P0HIIS#%BnfB_29653vi2ie|UMzh|B{;zYdDYFZ`Q(2W7H@tJ?ED;98^ZL;k zY?S0PztT1-OxJg)pe+7uRG5CP^?6d#M@k8S43$9~-jDk<=tMYRLn9Sxms84(^Lklq zF4X~nA(hp@j;89;dpxN>w(`xK@p9YuBZK8U&W~Pe(R!CU+)An*btl>!7;SXP*h8o> z45+gB>lT<qnP-%^tN!3N zW=|ux-VE`tYxq+6=9)3nBg^c*hu9K{JNR_T%RSo!|FoCPr2zS`Ie;FzGD<3te8OS@ zM6)myvZ+xWtVzNYjJW2dQzca>3V-5;962J`>LC=9NqjQ5X|rfbLD1cX^$<0YPI1|! zZiB70<_Qr4Mvcp+&*9UH*#b!X2nkDYx$}TR)@A2*`)hlw$*{urz;{_{9Lic+DeYiB-f^KoiC0)@LF5+@Ovi z;mu3uA`kWh9OIgEm7g`Ps_SAGyYEWQGVCBXH`mRM$2V$6`(daP}Q7-*fpPg?yCxEWZ3@bWlDT#qln-WmVE3+XP{9G|qRTJ-;oA zBYWG#NLuEKh_Fa}rrW(B3Tw&eb|7_V%x(}L1$O^S>(#Bf=Y3O4c|$fw{iUn99HmP3 z9Xk4H-t*W#{%#dO(_K@#?}camL6?2sS|rf$u43nBmWhV&IN~!Q-FZB@c^($J^1M7S z5)VX$psHWWQise5)b7jIbUcrO>|cekBK9#*Ix_5{zP5xuAoej{q#YAFZEsKfp#YsZ zDA%o)aBrnjBv5$PK6T#fOM7AZhQ`Jj3**9*rz?JYJp3em86F>gW>c66%0OpP8r zee_RZF7BPgFuG^4Ah!-*Wp0Ydw)ik(~k*P46Nh^)2| z<$Iz?NY4ue6<~Rp4Qknty&SJzx7*X@S0>*JFGEV+{fgTBejnY)SkU{Z)~_AApk?8K10AWX%qjfcTBGQ@PcCDCm^fMwKKxOA$m|V{ z=;&X9!UJ9#aLi-JKpuL<#N+_}Cy6cfC27yKr&W){=0ET z1Hcp$xVH4IEO!?wvj8qL1UzyQqR6j_&GxZ&ybd0bM~>6SM+zwqf+2fup+5O#lBI6w ze)~c_O;bKiAoR!j?i52neS{QS#3Px+VmF=={v^Eu0-09BT_cfpyX>kaT6aHZB3Mq% z3YH!TSPy(Es*fBF4C;oM!22+`1H&oD1#m?l=n1fzkS*Y}d!B-Trt?pU;n9h}y)XEA zWLvpL5~tk#(vRkXD(9_5oIYV_>A}z$X9$LgEYde)V1o?qNJu-N*$7vy`wO+#8N=;1 zI8AWnvRfMJwwZ=YjeL82-Jt#o38`gek~Se2hWQzdlsurY<9LZ3@|sl=hA08^rrl7F zlrf_%FJD=<=mTi}kRAi9_onXD77-#`tLuQ=2hB%&#naVb56r2wz8{B_xTQG;4|YOv3sU%+QICF zok_5HFHU1OFGfBCB(}!tI^{iK)^^AiP9L{ZUyjbWzaSBdItW5^>w|lm!$=pd1G-G< z8`W>N<(nZ68bT7q@#4^kJ8%k?GY&z)(et~~O$cxy`Bn?K|MfIY&tpd^epsA`{Ri?` zM{NgQ#&|D=!{j%I7BSIJD&hke$r$li9yI|&@GEOj7p7bhd$7b%lyeZ9_`lE9HQQ>TvRX06*K^F7()U|+fUqGB?4>2z|IN@BM%@3 zgIZtV7UzeHhUf^b<4Lq} zwID_U?0;-#mblyQz)3`%dF_qS&!0$+*uFevn5HE}kV7GnCg4CoUP0itc$3S5`W7+L zbtSGk;Q8C0cUZ6=P*T#`93=v~va848X&lLEgTQ*f{RLV|sjWE>$-5Cr@pN`2po z=;GQfIJayUexyN|LAfKMm{?5P{D!QuDrBB~wF>#_!Wj}KK*Or7oQG;sw=!9Iv(L!z z<2WA)Hbkduh>7oo7;4QQdb@2#kEU+rQ0X?;7>E&e1IZl{9ck( z$0w;DT3X1{Bc?in_D=9;>!(Q;I#0nbV$mUtNbu#D{klS+tc)SHkZ0qmJhLbh^bU~JDfSSUW-{ic+e#sNhhIQ9Rh$f`gGccBV)=#+o6#fj&-Oi=@5%Vm zJK-s>zwo1$5UxcLh5&;vvmFIg=q=YkP!4qw&o_>fmX6PZOhQAHR!+qgd7EE=U>_;i zkfumyQhRb)!=9Y0zrq@jXdx;s3HT%!YsJ%36{V%)p-=@x^3xT>Ku4^#AQyg(nqwP0 z0}%-7HPU*lEA}+xA}v@SX7lSOhFzr0k+mwzJVpdhI?SH{sPICXFxpkq_^}i*^4oE3 zU01ulhjxP2-i4@x@*u5@pGtaYj8Wz}0^pxSC#p?Tmg4&2Ri<+UuR?&$rg+Kg0n->b z2cq-h3Xf}!%&&3X%NugmcW|d~Oota89(Hdl^w3R&M^Ijqtg_}HUjvUpt1LAVbhIAL z=(zXj4P~hS7tCR40LSft&(+~GlM^=z@`o~n$)U@j!7_x}_YK;_-A4{^n(v4-IY4O{ z=hW(I^b`ayvE~36sbd=2Wo4ksS$?&fZFNY%-6t3ox%PTbiFf=~z#^{i8!=L5_B_;; zw`z*h!4~*)WANAf-R?@#%#~F!3xp0W(ax;$i^1Bdm=R-_y{#>{$C08D5A_%?bc;|| z5d7ysao8;qSGn`O?il&QP7g;ZoF)uC3t=L5xZL23QR+3=4qXsL3?&w;ullLAT0r@? zfN~1NN!bQR$`Rq?6*(EASA3*ma8q%M`o3De#%$cN@Z7P+wfEJ8)b|3F8K!poOQ6vSa4q>Rs5 zBhCb80^g|6-Z^FAP+Tr*-1Z1kF83B2D-hW1L{M2tBC=D5m(EjvsRx3jHthjEPl_bp$#Q_wPDGfp6ZdF@{csNSeWHljmQO^figHlefha=a#3jCeMOa zrls8%KGR)|jKQ|dG{)Xmg%_>5akLW=Nrenqe4Jx2p+`6womseg(sDfE898^r&?C=` zFwEQ~MjV>5ag4IJnz~V7My8zLgt1u_KN+pc5mM-(W(aHs;9!8D2u;JVu6)uNHoSs zpGrAnF_~ompY{7YFjgvLARPjqVwQusMQYbjh0po6*E*J9;$gsLQ9v7czPXnzsYExD zl2!K`yL6Y9_da~&qEA->)r^roRD8HWD&d0FV|1Rg-VRWRe=kOAkRv*NmK$W{(Vc!@ z&RJT+h2v-pFxU*5HW>jSQB`(4+}mGs`GJX0gpaSXYpbbGeZO3O{(+1sy<+kF_7>1y zPn9Ep9ApD)V>cSG1!$mNioe3C50SBPLq0jOvO_kC#Yxo`SHqz^#M3L(Pkn|CjahNF z!KDi2l{x;11v`*r9R8R?LZli2e#M1uh0<^Ikf3w5wbo4rrtvinSLwEKuq}u@JZJan zz!JcEfM6&pPRu3j&!sGuc-L zr%y|hx$Cj7BZEvWqPTR$NX9R^GlM3V5HmPlbD;2u=01tYr9YkuIo`2b`4z&4ZK~@; zpAb4HakVOXb#ynm;4RTFOOUJZ(Sn!#CxM9#udfI1Hwev0P~Ohm+LtU${hO7YgX1q& z0r8rE(>@IT@gSM$gF9Y2CIL5MzgiTb*q##sp3>z96%};Xhs1TUIqZjPOyNs(Le;Qk zeT=Azfi#!rg$1~&V}S-+-D(E4YD(MX(ts^l*P#KXdNbnW1@&K<;#*)!r=Y@8lL@A) z8XajngShilhVuq)j?lU)YI%HzDElEK;Fq|IG&bWwG|xh~3 zMu?^kkht0n>)$!5o3i`_5Rp0_xA7_j`qj~N))8Ry>B-X6-fQhDFu*A&9r1X@VCapF zk`|;~8mL1u>YVFM%MBUJSfV)6g_F~0QZ|!ytZLnjudM1@hA72+eW_T}fua;CmxG$H zMiLLo5IK(?3M)JrCQ(?5!CKWmel}-S(iD;p87Y%j1u#blU5RD24`ZeAiTW!hOvMjV z)bE&an-o>7@*4~=4L_E^^WFr(pW=orX8>`0b(D7+J`IkL9{;4bp5MSqmNK8al3YGooPwB|?xlm6UdV()!1MxISK*iZ}6(-`ZM?K;)m^(Siz zc-L#{oHtk(jS>qg9`hElvCN9%<^*n@;*eL`P$Bo5{ibW^%3<)qPpxzlw_~v)Yf)Yzv$r{IYh< zf?+Z%tUHU`_Pp{cS`(1unui-=VA*H%&O{^T)^j{ts`idi_!qM9Rcib|h8ifDORM-+ z8!^tHp7rZjRJF?rG&vpHLU)x?0uaZ{fg|KM~r4|Ba_f}5OW0gtu!$}rm!TpeWMD$4*VKpnAtmSAyPL$a8$^>wLrPOV!Qh60XBuk zGOay`7rjRh4?O6dhSn4(8;YewQfiPJWA^O2(#oVr&!xS{f zBcp%R)n!KJk7)>mUXkp5Nq>5Ey7DY+Wy+y;kAdMyhdqwh>gE8XRQlGbZ_pLVeieb# zrW+%Lx7I8pOF>Me?>I~>V4gxc3&H@{RS;+ZmW! zvpL?a4idy-*(>iSXoU;FKUx(EosM@brq@CG*42rmi)XK5bZ(URe8olFU-qYslZfT7OxvB!HkSyYhoNjy2h62KqZQ(b`-KUrh}+JblmpSQ?!-4zn-mS;(O?&DBd>mk$X$WData30l!xeToQf=p6Z$y=sJ~;iz9PGae#!1TzV-7e zRT!7cGQy_G7#A4iWI<@Q_OGo3oj%$7nKa(r)dgJkHMqJB)%vmrmB8)%fO0_@+LPwX zKrtH7OK1O;U)>P_Vk(hOx@8ESLU{JMwv_B*KXlw!TBNHNnzZP(I#qbwk$o!i$~d+R zPuCFMij-!(hN7#7RDA7no_j<}=sEk^XJDshZ$zDAiw}0an$7Ozh(l?gz#`UNC}qRW z%JSy2^92Zk2QitZWK6ZL-7?XZzN7h$bbzff;l#N0P)D~0;IXz3qB(@$*BVZ|&7^lM zPew?yZZ?5t8=iKxAm)6FDMR6}3UBD$5J6F%Y=>!v)i{O?vWJ&!cz3GDHka3#tH873 z?1^+)nS*{%dVeN*O@I9ORoa@5`hxDu{>37aldGjSd4+OxNGkzPHsbdz6H6K_D1b#K zofv!*z_-$UWM6$&uk=jh$sUTj_0L|C%gAjMNr!I2dMn6Lg_`S6MzwMPJYeh(BFC#C z1u^xk1?$61Ui+vkFB%2XZ_J&ZXW0gQr;TWTLGn$2!cwRoX!#aVQV0R2dn(oi*nA0p ziE!tX2CJa($cBGlTX`|>mlVaYZyz|FeJY%|j5t3YNY3Agj;SCANN0#gk(ph2+kZVl zdOR(4tVn??@baT{eZGyw&6B<@6lrfMSa%R+@0T-G^%+tg+8`Om%E!;>f#x;4vfE{JGLZ{W`GE& z1W9%=;`WBHnaQ}|iQ~_aNWKsicmiomfbcZ3m?zC)!6fX+jYZI$6;usXNDk4i^Pq&l z=<8ygP07wzu|pAqc^w!AP!AP0F~A~s?HXZPB57tx1T}E|(VkAsI z;^%puxU|-GfJtTcbpMFxFauNgV3Jl0Kq1*%{FuAMATTK?4z$y?N?MQ8o$t4CqZCKT zbKV;dBGbplQgkuNS8)Lr(ZPMXhKo*>@&K9&;UNL0k1vo?K_%1@i(Gk(D7Jx$z-Y4R zrQU~2B^V;ELO3|jsM@uJM|qnnxQO9sWUUUH)97(4)g=x$qgij>&`rCb6%a&iPI>bs z06^DgB2U-i{!BnP{GIrtcBnv550lh6&7>e^SSX(N7W(Hl0HSHQ!siSFPxy2ds>vuT zH743cmOZCghMCj>S&uB@Oevp!Ass4X(ANVP{Oe;jj54Atd3}kT3vv7(3R7*Sb*mnd-B=$S)N#PG7wpGT49X3aQ+Et|0&Q08k@^*PX^X z@g(9~Y$>fx9s=A$3s54=_SLXe6VUi3EgDGnPNM2nCI7owWj&v8UqxCr()Te)Ld(+h~Z4s$A7&RnHnKn4rNQWMtoc%+}aHE02P z&;gcc5t_NcDetQjiGYT}_)PJkh=%Z&cyNam(9)Bk{XtAzr5<@%u4Nq`1bL+&{5B>& zKpJaiY_OPe{+zI?M&$D^@)Twqab+cW=6&jhu|AGi$KC;OkeQ~s%)aX?-&v_Wdpfx2*csy(xub#xmXNDK+K_mA1bW~lX(o* zDxL~VYjr^wkRcRrQRfgA3}9Icz+BKHV3oA!i8GMd#Z;9#Ui;!n$Agn(2+ipm7yE6u z!Cmz%Z4jh{M39td{uvi|83!s@W&zIZrinK%5)PMQO_z>Arf|gsv$nPl1Kwg}iT10& z>o~kkHynaps*G>(yNAV}<`Z_O;U$CvvK)`M0JN{y-PGLi8JXhED|o^)3zano0E4&q z=6F3hT;P?E&xX1gaf--wz}-mN?W8rm9}4092k)KMM-an@2@sEW>2RC5CL$3T@#BeG zu+#J~X%!6NhY+5aJmjG0k8z0O{rUWuXbK2Uj{NIe9XDh|41u)at<{9SiS;YvX_Ew@ zh(TpNVDo`afuAy9W`*PaqcEF|vTC$CI>Z-3_F1$afoSFM z*Jz;ddsNJjs*VDt$0??qe{`8tAU`z(KWbfSyNbm096A(rP z?Y~cU!GmKoo=beq6Fx}3W!F^)S|)%Qa}}|2ZXL|f8=8wS=O*GyL)vV5?}@Lb04q`( z!00dlw!7pqTo_FP&|=(dCs@f8xDc)hOp4LQXADZ8RnK`AhcL-&Z39G>@ce4dwy}%8 z5Ap$XffUlwnRuFQ@C=QR`e&+Eis-R(Gte4RP9ML(MH(U1Dz}f}M36d1P5WuoRzUv00&o_O>1LG5^GLBI>^=)!32bryFQ*Ft5P;o% zu=lJ~0sxl-mesN=2TAEHE$2bVUH;UrejPL+=pmIU`CdWm>H1rEu3g zq#H+_?Z$7Uq0$MTS`15TB})E5BClUHHlDJ1i!Hqyl&KW z;Fa1E8W4lELHOcL;Frt=3rFB+i#^v?PO;*I(<%6QgB(pl-{qnJ<@s>`buhyO;?^W# zh0C+v8ZZpyg|Ko3OfL}An`OdK1Wvaj-GpNM5`a~B(>wt0w84mEDj+`qA6)_0%oU7~ zjw>~A*<8Y;I1oOw7=Cl59BscZ#7F{KdfQ?c9%Y!6*bRJy22xb(^_jr9n(9{VG$aia%6$H?58is}?^h<$_bK&O2K#|o3T4CZB zITt>>3>MVN`H1cPaKeiI+wlz0Wj1~^c&ZDIbwPhjzQ^-8;Hv+hG1~wD6n9BPK~x3s zwN*j|C`_>N1D{(2E9#)oFW=*(GVpdQJaiiH+jE)azp)&Ej&2y1H2ZFP8LVFjx6MP8 z3!5q&a4B$GoqkBdP1E2r*FbHVoCMVufu1&?4K%kRfXjB&zS;z*I^}#k2Qa?|e*an_ zS4o1Fv8O*c>1(M$GAiEr?=6DAxdrOVteXJYwxtHIG{75Y0S~riefbH%2hTLZ;bswW zmC?@%d13Wzc;rTyQ6!R9JgR%pn2|||ll3$6;O>pEvR=L`zft+$7l9w{ftNbLZR6oF z(i4E@1cu<)89360w2_v4Kisns?pqB71tNn{iGVR81;0@bx2=H|4!lE@Q(IbjKXyl z@bulVZmv8Ne%=q^Bphyo7n@)pgYCP+*y8aG4RAr@AUv@{bY77e)rGKmIehCjpdLV@JhX&wErtpOOF{bzGxw@2l&2@Ve&HsJ6Zq8v1* zPTa#wsRV$UV%WSA9&y8SyWz)YM1J70esrkMh2Omz zZeA`X``Q5=eb8_QIy*qi&5mktr>&k=6PxGfN8#$(P*r02krKnD!yQYZDhQ!m_~tIe zy|I3-=4nbm2MlYdWEDeX#8`m<}<3G^W`*lKd;bIKp2xVn8wc?Q*zlEmRjl&?c?bOcPoM zpy3!CIEhI3QUox$8|!?KuiY$@E^y(^uyQgf*h}Xp5LuOZHf3SnhBNrVBcOi z9)^=?h}n6AX)wt3xQ5E$iWq<(4utL9?l% z=?cT^C*h}!7kocO05Un&a4-rFzYI&ugv7MlUeh&jJ0af*pIil-=Rs2!Y(EJt?eOg` zpj(g~KENyLBg|1Eyuzoujwp5_21J2eSY8GjD}_6D?Hniyi2$E&!yb7T;!Ha{yAMd& z-cquY02~e&Hep*6Y&{4*k7(6pQ{_y4Kzxexp|%tv5xA`vy8Gd5H@wgS`_6zF7A}xG z07=J{Ap(~HsSz;Z!syq}fZM8}xL5@BstO^HBSxrY1Dbnb$1!;7n58yR1R$mIL=z|D z=eI$1J`@BYWb+pmbVJaZdpDz66rXet!h$wvZHM-5*xn912M}q6uFG@MYJiy$Ct%72 z*G+?^r7*1wDl1`8CFBJmHwRohn!0I*mBr33u+2L*R!+S zIh7GNOh4NT`Yuh~pfBG$W?9Am;R#LgqtiAvKtoR~U z5>3Z2=t-D2-F|n@7c0v(O?-l0af@*4d??glTNjuSF<z8yLl7auc5k9mWDhuSTx|~RK55q4G!56lQ)NF^tiZNw5vZ_Ko0wQ7?gO5EA z`%Z~Ig4qrYsIP!)m%%-wgGtYSirq1wZJuAPF z6BvPSy$lV_Q|Dz?mx}B=JmZY+rlDMiyVr`l{HXvFaZo(77mhbs-KUsZRhroe7d+Vj zFCTytZByfA=6hj&DXb5Ge~h)pnkD^<-EiX^$n#AN<3H5{Ti<|3_5%4+=dc)sGh4ry z2AFPmcsB%`;J*f@6=wenb-5rf51eC6S~t^x7jmn?@6P^p%9vu-#A93GspI0iJGAVn z@|*GmU;;ibv<$&__JYd;A6h92a2#3On@@8y;=N1bqILIQMi_Z5;s)l zX7^oxTR;44ANw}zUxq*54)`IhW;IL=0=Q5fn=yFk4alTn^I9m+&93jLTmGzLtQ|)U@Hs>w zRkjg;VFmntvJD0Zbv72;&M`c*WiZ-z>z0%`_;VGDNXM)Ogxd#;yq9{iGt`^+EdxJa7OSBQP)wt7pUXVlmLe zj>fNzlK@6+8X6yt!--aStBEEU5t}Ih6X+X(U5)Vlo$&YpL?@pd@%PI36#-ln(xW~217cTw>1S}T z13rB<+%QiVh+Sd><8=XI8BrT=2b(cX@jVA3aQ{|#aIZM^wVbI_%Bu+AvRRNgLAITM z?p}C(34H!qF_PR4Ct}7NhY?r=5J_MQ3yP#**9rLgOK_|ck=5f-#;*uqq83Jnt~7LY z!Rx!g9fpkyVPPE<=SUpWFhm!m%vb{fIz&x~NUi|FDQNA6y~p6`1MvJQYZqHrpNaq! z0Zf=npt(UeATtE_zX*4A!|i>rxDLvTw*>@MsWFCL%V(_c_)oDmlM*6k0mO&h-{vo_Pq002JPP0_mUyg>gK33?4lJYpdZO zZ-y0hkRwSK9m9a(gq86&78gK+UIU_%Tmzb>__Pj++WkL$6-Xd*oUK@lGJZt>S9Vig zU@Vw$um%3|G1$BWu3HXEs)SOQR2Lu(o&8p&+Zba2iHsnC$-V|m8PM7f-`Nc(55wVZ zgrsqh^@BPT0oVc7CX8iZM>n+Wf|f2=Py>r+h+&!K`J%e+$}<4;^o>sekb%M2Rj@9BPDrY65zN;p^V1*RWF&fb4L9CkU-!cxV^U0QW6`rPEo((%)LY!PW8bP`{D6};(5>QJSXP@d?~f7F0b)x7*3tEGvFo?P7q8h z0+{$XfoXk`q9aXngHQ&gb#UEmm|Y4%pJ?JS5sN<(hBF7NOD2s{05BJm{tw2B8@9ulxD zp)m@L{XhrM4(+F)wit>EMN&~+A$Yu^GBM-_?-&@NFAD9wu{^M0%9>38G(jQIM@hVkBe4kF5e|eXcs(mNCb#t37A_6p%CQz&L7y(v`89?#UY)9 zcmjIDVm$lEF!T+=(H{6tn?-s_AOPMRh7PPDP4HAcQ# zmMcpN4pZFQldufL4ftVx5ax#4w= zeyY!{;0X-6ai~R+&hX6({=4x@ACoJqp^XptT_CGNjZCR8W{Y3ZOGVVns;n z)dBK7#QbVm6cnZef`H%6u*C(iU>e7(ee|oPQBcT!RQc&zG>wLH1hBr2os~Y0D>pzv zA^YWjxrgIxXR-4f0o=8oH?$1z;8N?OppeyYLMBbay*IG^909BdX}u!`jrl4WSV1AH zQRsr93{5jMZRi{U0C);$nI589Es=sk_M#@lsZb#;7kC5$>+3jK7ve3oL<$O73Jq#= zckzXPJQv>k2Iku^kT{*hy$Dp8) zO$g{DHx==c&(Dzy|J)(~%>-D!oc5Yh9*mo$(N%V*f-$wzQR{{6Giusj~aqlcYdsL4g z1qGYK3AG+}f8##xe&UJu4vcz_iQj*XmIe-O__}$ZZh$3C!?=_$prBw+)OZQ!<+E$c z9PRW6cn2TwgOxnscJd4XwTKD|wuB#?I?v6hI1x^l7$o$uJo}FbAue&GMC@+d-?ldkj8Ox$XM}id9n1%=Jow)?$u3h zJDnhh6kw`hQ3?u|$V;yO%AWTBxtWF7YyV zdKK5SM7XAHm_?l#Ac>eNtDvCpUW0c*z6)xCoT&@)dUHQdYjAXK4Q-&M?$;*%S6){9 z)r;n=-KUwqua$}_2Q%}1Ea^(Hpe06ecyt7|0c21K#tKsh4(p?dS$$Oj`pUf=PbGMJ zz~EF~A#Iyxar7T=)DFI%yKNf+cvrN*G#!8a64$+TicN>lFt0mKpvZx%&`k&j1!03w z%)k>ja3(W2GbXxesV`IMJk`UR?l2XDF>;3!1fxj;=?pGYMG_S3spw6o zgOuM*B;;Wv&&NCbIsr>1F>dSCYc0ndrnDFhAxMgRZ+07*qoM6N<$ 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 zcmV-n5T5UeP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3&scHBA+h5vIEy#&s~aya3f-a#+FKTuM|PVD44 zepafak~o0C#l@v!_W%AX=0E&JTdIkv)ZB8m{KXcV?_5-S{k8M$Y&_rR&)<*u{qDYb zJ@DKLoWu3A^t*lMe)`ejnZu3u_f5T@_<9d?KX?WVIBDY%J znmTS%=iTs_a)0VGS-Kzb+q}II3tn1@iGue#xM0tByetUUr}N2t``~EjLOFMm;p9TT zEx{r0miL^kee0liPJXV;&)$Ds{p5V#@B85=U*_9KTzvSzrH`F|T>NI@@UqCie=v&O zzbxneh^#8IuhsRK@o2sgb)4;Kf}{6pD8Bg_G)_?uC%%LhR;a{p`fWnswtv~;&b;l+H(h1s z04`$ggfT7<0h?V&E<0PiFV5k|N`N|+`w;yg z0xl)LFw(~mvNy$7uXEFN~$Kc z)N{x&r<`-iN^-q~5=$z%lu}D8y@ncVs=1b0YpcEa7U00tax1O2)_V7*Gf`)(PW#TC z;YS#8q>)D%b+pkZ@R@O@nP-`Gw%M0oVL=lsud?cDt8ZsEOtIrmJMXgVZo3OulOR!& zWGPamNqff3lrkL8d?TF@MwP zqdw+uI(^i~e0NDtAM@QMJ$=k~m-O^8-(Awv$9#85PapHUPKrOxI}d7iPjhTZTgq;E z^|hgfZHrry`p#lX2G1qge&9Yy`}s|n4tKsK5Hi=cx8$aoL2yA>P% zKIzOK8oUhB^G;G*ke$|LMGR}t7WTqIaP*jzrj+_u%hFYu-!H4L<@27fbeMc=>Qk@# zqGok<@t%8a_%Mi3ic8(K2#>5G6U!~y8%{5y>Gon3_7}rpMx&?3CRA|^%IJ_z8f#J^ z#xlN-0Ud=dZ5GIy?n+0hcEa#KO(0<`m-b3&x|Id>;Dng#@Fol8skfGqa#(zIn02|B z=+fGz%;+_B@RxIGe7Y<>n}YA0FSxKD7KbOBR~s4_JUtRkfbg6Cci(efvDk(EW~|et zQ2&;yUmfi3zoOgrojr6-#ceOHa4{%pqpecY^kP=_LKJ59tMro6qQHTc8FvT^UsB#G zZtW$GM$2nldge({m*B7|3@(gg<|KCO;^q(I%55A4lCa_2d`{cyyl&6sb>WMt88uT{ zjc;jFzJ-zwG$jXH8W!6VYU*PUL9jC&=AYV|ha-^mUm+D_gya&L-GoIH0W88rsQv`C z+)z*G%5%l$;Ua>E5U|@v;lz?;S<07>Mu35*WL4&`f)#1nM{Eg0 z)IwS2&InqOKWBsuP(fA-%`dbN8w=7hpNL?BVvBVlAEfQAf_FnsJsF-NCPFW>J!@hY zX;q=@)W<}dtju161fkf%Y*1Rlthi%e7=RFBamgCe&s`?X32bEdHJ38sE>oeQh^Zh3 zAMd)b2U7x$-lG`sxFvwDGOl)3XVJ_y0E|u{c`Xq!R#3S2_3#A z!w}F|wt@Qq!DQ%0*?Zv51In^~;LDkHG zDRA+vx4FzjC}$S6;)B*Id2VnvZNIR_&SPwr?pk6eQ4UVG<`5Kpfa>z4{2^g>EdPK7 zf9HPl6&C!-{hCyopoW4qY8WT?C@RaCwZu4p_Y&r;lAR8|CSy zR-uo^uoC8H3W-kaCP}T#Mbry+evZ9KlSd=X*~T^(FND{+9f7bt92&giq*X2hkq#}z zyYx9gE)|~(8)yx&(eSqI-YbH?5CrBUMS!;blO$-2PBB!2t6Clm8*7AsR!4FYCu*pv z0~U_Y;0%!YbSqxX6ZT}33k7^qe`%ayPKpwMd>|=ygxEt-a#e<-)E~h>ph~r$VZX4` zrsyOy&#oMdj@Bs>YbdKhLsvxLSbT|x){CfN?mo4~Q@yAonv&SI3^{s;1|yGI?T}Cb zC2!m^d;zGa4L-zJ&!Bb2rqlVYYcyyS$=pGqrkDH?PcF^ZDqg+yEXmDKgn^3oK)3=e z3giWayMMCKJMA}02na1pKu6Q8A@t15lBcjmR=!mhi7b5x`jqhNdjg9jK$bOo7#DW+ zBO;-KN;DU|CJK4d6__vp5vBBQRY?Sv%Itw_Fn4jTAwgqjSAI+F9AbiwvNBoH{+e3w zM;!TJGOd{itis(>?@iHYIT8SMa!D7o1xP%COvOtG?EjgeYwaU?{oag`TMj2tSX85I zBRDaR;~`3W?~Y0O#5XgQlwBu1(opZXwup&uxHh$PI&`mF3xb|jG|;5vhiQ5UAM=~? z)9)G1{6Xjak>6en8J5S%_>6MU1;MMYMW`Q2(lQo4DcuQ4;&2A>MyygsaAMSVSzc9B zh*)VZ)aid0Xc8)oWssV^O1SOT8@`{G%qa6R3LV{5o*)3vuFIO@kQVAgG&l+UM%iB% zn&q*x!ib!S`V%$~3*JhKObI7B-EtxQ^nDNiRB%j4;{vhf?L*!19Tk6~#>}s~)4$uC znqPOPpKneTPKyFCZPwC0P_?-XJ=PCZOW;y#=r)2p^j!kr-A(%C5S3I~NH#uix z%2T_J_DQ;DAfq6T+nGJ?D>mkxy8MQWFkjnDeVAq+SoDpv`R`1C- z57QU$LsXnMq*hS%?2_2Ab{$+vZQL)cm@P*!&_sH@%hGqGj*sX|Q$^`{nW|G=FT^yh zC;>ZMF@D?He69KCtiyL*qjcYshW zGRt?1$8Vf-E(<&}WF`~y#35oa*ThN_v%IMh zPZ5VzO{aVz<*~|ni?dcNv-&;x3xgSbWtr+gyS1{D6JAm<3bemC&c`qi*ad18$N4^XoZ1QCe+I7f zhQCw=W#$(7O#>TsJgj54hX`2A&MrlwHYBQ^;n4_cQvYG|+bobgy~6)%S7w z03@la_ziGy2#n+?d%eZGJDYp^_e{ONA3k_;lIbFEEdT%j24YJ`L;(K){{a7>y{D4^ z000SaNLh0L016ZU016ZV^=n(?00007bV*G`2jm772?Y`;nIF3V00Q+%L_t(o!|j(% zNK{c2$A2?wW<`EtN*`9Jh$w`bh6$CTAc$6`wkWJk(ib>p6uM{;N(C~4qF^KllBnn+ zj222nWMgSkW@QvbX<&+Cs59e7O^c_z_hx)E&rzv`2j1eGckey_d+z_7doHZYKNgLR zMP~^HN`Yj+YnA*=0PR3Add(+g06I$u&;|Ic(T2x?IP{v&iw3RLfRZ(I&cfOyQiC;s zWL3*p7QRLKvn&uP_5j#D^Bt~GkpO)c;c2b_;?92fdJP_B!`Yoyz?K%74_CE2z8MAQ z)4|tMFd1iod58{$-GRcF5)0?vLqQ`LrqzW*t7cF`8Wf!poiCj}pw+^G2zYP>GW}Nx zK=mQWNd&FLb-U{)1F#r=&@;QE9P?ZNU9xDWJ)DO<2f=1%Jvk%frA*o&nL(wY; zyaAQ1Fl7?Q`FlfImSaxX1(2T#8#jo0TPGycLi>dL49pg|{0uHsfzd45<0Ij?zmouh z+#ohw)Q3hOv(-N2`XA8HAz`=+oQQD}!09mXa2NH52AH)tE)V6MFf%9GBK9~5AUHs- zcYjgG>V6}P{StLwFW|Qm0JgZxb@QD1>upXl*4@oM5fuSU&dBvZwfCY0Hc8UYG-IDP ziod>(%JqX`>ilPH1D~y;K0M|mfaU=?nT|w5l!xQpUrGiS$*Mgcog`tu8D72z zEt$?@!`E9eP5!dhyKwIS#MUTepk6HQ4lkqYJy6jCvsP*7=Pl&- zc3;~$UHx#Z)^7SQuOHAE;PoIBW*TfYg`<-d@>oyW&b4xtk400000NkvXXu0mjfN Date: Thu, 25 Aug 2022 18:36:44 +0800 Subject: [PATCH 217/224] 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 c04168eb738620f6002121e164b4c5eb998a73bd Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 26 Aug 2022 12:00:53 +0800 Subject: [PATCH 218/224] 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 219/224] 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 220/224] 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 221/224] 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 222/224] 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 223/224] 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 224/224] 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; }