From e0b6c5d245983e79bb56d558fc92775cb9f9f8be Mon Sep 17 00:00:00 2001 From: turtle89431 Date: Mon, 6 Apr 2026 12:04:19 -0700 Subject: [PATCH] add ref tmp --- shelled/{plan.md => plan-os-ui.md} | 0 shelled/rustdesk-as-ref/.cargo/config.toml | 16 + shelled/rustdesk-as-ref/.gitattributes | 1 + shelled/rustdesk-as-ref/.github/FUNDING.yml | 2 + .../.github/ISSUE_TEMPLATE/bug_report.yaml | 55 + .../.github/ISSUE_TEMPLATE/config.yml | 8 + .../rustdesk-as-ref/.github/dependabot.yml | 11 + ...ter_3.24.4_dropdown_menu_enableFilter.diff | 42 + .../.github/workflows/bridge.yml | 98 + .../rustdesk-as-ref/.github/workflows/ci.yml | 250 + .../.github/workflows/clear-cache.yml | 37 + .../.github/workflows/fdroid.yml | 39 + .../.github/workflows/flutter-build.yml | 2062 +++ .../.github/workflows/flutter-ci.yml | 24 + .../.github/workflows/flutter-nightly.yml | 15 + .../.github/workflows/flutter-tag.yml | 18 + .../.github/workflows/playground.yml | 418 + .../third-party-RustDeskTempTopMostWindow.yml | 60 + shelled/rustdesk-as-ref/.gitignore | 58 + shelled/rustdesk-as-ref/.gitmodules | 3 + shelled/rustdesk-as-ref/CLAUDE.md | 91 + shelled/rustdesk-as-ref/Cargo.lock | 11288 ++++++++++++++++ shelled/rustdesk-as-ref/Cargo.toml | 247 + shelled/rustdesk-as-ref/Dockerfile | 64 + shelled/rustdesk-as-ref/LICENCE | 661 + shelled/rustdesk-as-ref/README.md | 182 + .../appimage/AppImageBuilder-aarch64.yml | 102 + .../appimage/AppImageBuilder-x86_64.yml | 105 + shelled/rustdesk-as-ref/build.py | 647 + shelled/rustdesk-as-ref/build.rs | 94 + .../docs/CODE_OF_CONDUCT-DE.md | 137 + .../docs/CODE_OF_CONDUCT-JP.md | 101 + .../docs/CODE_OF_CONDUCT-KR.md | 133 + .../docs/CODE_OF_CONDUCT-NL.md | 136 + .../docs/CODE_OF_CONDUCT-NO.md | 125 + .../docs/CODE_OF_CONDUCT-PL.md | 133 + .../docs/CODE_OF_CONDUCT-RO.md | 85 + .../docs/CODE_OF_CONDUCT-RU.md | 134 + .../docs/CODE_OF_CONDUCT-TR.md | 89 + .../docs/CODE_OF_CONDUCT-ZH.md | 87 + .../rustdesk-as-ref/docs/CODE_OF_CONDUCT.md | 133 + .../rustdesk-as-ref/docs/CONTRIBUTING-DE.md | 50 + .../rustdesk-as-ref/docs/CONTRIBUTING-ID.md | 31 + .../rustdesk-as-ref/docs/CONTRIBUTING-IT.md | 37 + .../rustdesk-as-ref/docs/CONTRIBUTING-JP.md | 41 + .../rustdesk-as-ref/docs/CONTRIBUTING-KR.md | 46 + .../rustdesk-as-ref/docs/CONTRIBUTING-NL.md | 50 + .../rustdesk-as-ref/docs/CONTRIBUTING-NO.md | 46 + .../rustdesk-as-ref/docs/CONTRIBUTING-PL.md | 45 + .../rustdesk-as-ref/docs/CONTRIBUTING-RO.md | 31 + .../rustdesk-as-ref/docs/CONTRIBUTING-RU.md | 45 + .../rustdesk-as-ref/docs/CONTRIBUTING-TR.md | 31 + .../rustdesk-as-ref/docs/CONTRIBUTING-ZH.md | 32 + shelled/rustdesk-as-ref/docs/CONTRIBUTING.md | 46 + shelled/rustdesk-as-ref/docs/README-AR.md | 173 + shelled/rustdesk-as-ref/docs/README-CS.md | 157 + shelled/rustdesk-as-ref/docs/README-DA.md | 149 + shelled/rustdesk-as-ref/docs/README-DE.md | 182 + shelled/rustdesk-as-ref/docs/README-EO.md | 148 + shelled/rustdesk-as-ref/docs/README-ES.md | 180 + shelled/rustdesk-as-ref/docs/README-FA.md | 159 + shelled/rustdesk-as-ref/docs/README-FI.md | 148 + shelled/rustdesk-as-ref/docs/README-FR.md | 152 + shelled/rustdesk-as-ref/docs/README-GR.md | 171 + shelled/rustdesk-as-ref/docs/README-HU.md | 163 + shelled/rustdesk-as-ref/docs/README-ID.md | 166 + shelled/rustdesk-as-ref/docs/README-IT.md | 179 + shelled/rustdesk-as-ref/docs/README-JP.md | 183 + shelled/rustdesk-as-ref/docs/README-KR.md | 182 + shelled/rustdesk-as-ref/docs/README-ML.md | 148 + shelled/rustdesk-as-ref/docs/README-NL.md | 168 + shelled/rustdesk-as-ref/docs/README-NO.md | 177 + shelled/rustdesk-as-ref/docs/README-PL.md | 169 + shelled/rustdesk-as-ref/docs/README-PTBR.md | 152 + shelled/rustdesk-as-ref/docs/README-RO.md | 181 + shelled/rustdesk-as-ref/docs/README-RU.md | 183 + shelled/rustdesk-as-ref/docs/README-TR.md | 181 + shelled/rustdesk-as-ref/docs/README-UA.md | 174 + shelled/rustdesk-as-ref/docs/README-VN.md | 161 + shelled/rustdesk-as-ref/docs/README-ZH.md | 233 + shelled/rustdesk-as-ref/docs/SECURITY-DE.md | 9 + shelled/rustdesk-as-ref/docs/SECURITY-IT.md | 11 + shelled/rustdesk-as-ref/docs/SECURITY-JP.md | 9 + shelled/rustdesk-as-ref/docs/SECURITY-KR.md | 7 + shelled/rustdesk-as-ref/docs/SECURITY-NL.md | 11 + shelled/rustdesk-as-ref/docs/SECURITY-NO.md | 9 + shelled/rustdesk-as-ref/docs/SECURITY-PL.md | 9 + shelled/rustdesk-as-ref/docs/SECURITY-RO.md | 9 + shelled/rustdesk-as-ref/docs/SECURITY-TR.md | 9 + shelled/rustdesk-as-ref/docs/SECURITY.md | 9 + shelled/rustdesk-as-ref/entrypoint.sh | 36 + shelled/rustdesk-as-ref/examples/ipc.rs | 90 + .../android/en-US/full_description.txt | 11 + .../android/en-US/short_description.txt | 1 + .../android/fr-FR/full_description.txt | 11 + .../android/fr-FR/short_description.txt | 1 + .../android/nl-NL/full_description.txt | 11 + .../android/nl-NL/short_description.txt | 1 + .../android/zh-CN/full_description.txt | 12 + .../android/zh-CN/short_description.txt | 1 + .../com.rustdesk.RustDesk.metainfo.xml | 59 + shelled/rustdesk-as-ref/flatpak/rustdesk.json | 66 + .../rustdesk-as-ref/flutter/.gitattributes | 1 + shelled/rustdesk-as-ref/flutter/.gitignore | 56 + shelled/rustdesk-as-ref/flutter/.metadata | 36 + shelled/rustdesk-as-ref/flutter/README.md | 16 + .../flutter/analysis_options.yaml | 6 + .../flutter/android/.gitignore | 11 + .../flutter/android/app/build.gradle | 137 + .../flutter/android/app/proguard-rules | 7 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 106 + .../carriez/flutter_hbb/AudioRecordHandle.kt | 193 + .../com/carriez/flutter_hbb/BootReceiver.kt | 47 + .../flutter_hbb/FloatingWindowService.kt | 394 + .../com/carriez/flutter_hbb/InputService.kt | 741 + .../flutter_hbb/KeyboardKeyEventMapper.kt | 122 + .../com/carriez/flutter_hbb/MainActivity.kt | 414 + .../carriez/flutter_hbb/MainApplication.kt | 17 + .../com/carriez/flutter_hbb/MainService.kt | 729 + .../PermissionRequestTransparentActivity.kt | 54 + .../carriez/flutter_hbb/RdClipboardManager.kt | 197 + .../carriez/flutter_hbb/VolumeController.kt | 78 + .../kotlin/com/carriez/flutter_hbb/common.kt | 157 + .../android/app/src/main/kotlin/ffi.kt | 30 + .../res/drawable-v21/launch_background.xml | 12 + .../app/src/main/res/drawable/check_blue.xml | 5 + .../app/src/main/res/drawable/close_red.xml | 5 + .../src/main/res/drawable/floating_window.xml | 7 + .../main/res/drawable/launch_background.xml | 12 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/colors.xml | 4 + .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/res/values/strings.xml | 4 + .../app/src/main/res/values/styles.xml | 26 + .../res/xml/accessibility_service_config.xml | 7 + .../app/src/profile/AndroidManifest.xml | 7 + .../flutter/android/build.gradle | 19 + .../flutter/android/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../flutter/android/settings.gradle | 25 + .../flutter/assets/address_book.ttf | Bin 0 -> 1792 bytes .../flutter/assets/device_group.ttf | Bin 0 -> 2012 bytes .../flutter/assets/gestures.ttf | Bin 0 -> 8068 bytes .../rustdesk-as-ref/flutter/assets/more.ttf | Bin 0 -> 1620 bytes .../flutter/assets/peer_searchbar.ttf | Bin 0 -> 1940 bytes .../rustdesk-as-ref/flutter/assets/tabbar.ttf | Bin 0 -> 2288 bytes .../rustdesk-as-ref/flutter/build_android.sh | 10 + .../flutter/build_android_deps.sh | 88 + .../rustdesk-as-ref/flutter/build_fdroid.sh | 630 + shelled/rustdesk-as-ref/flutter/build_ios.sh | 8 + .../rustdesk-as-ref/flutter/ios/.gitignore | 33 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../flutter/ios/Flutter/Debug.xcconfig | 2 + .../flutter/ios/Flutter/Release.xcconfig | 2 + shelled/rustdesk-as-ref/flutter/ios/Podfile | 45 + .../rustdesk-as-ref/flutter/ios/Podfile.lock | 142 + .../ios/Runner.xcodeproj/project.pbxproj | 756 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../flutter/ios/Runner/AppDelegate.swift | 19 + .../AppIcon.appiconset/Contents.json | 122 + .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 29 + .../ios/Runner/GoogleService-Info.plist | 36 + .../flutter/ios/Runner/Info.plist | 82 + .../ios/Runner/Runner-Bridging-Header.h | 3 + .../flutter/ios/Runner/Runner.entitlements | 10 + .../flutter/ios/exportOptions.plist | 15 + shelled/rustdesk-as-ref/flutter/ios_arm64.sh | 2 + shelled/rustdesk-as-ref/flutter/ios_x64.sh | 2 + .../rustdesk-as-ref/flutter/lib/common.dart | 4161 ++++++ .../lib/common/formatter/id_formatter.dart | 60 + .../flutter/lib/common/hbbs/hbbs.dart | 302 + .../flutter/lib/common/shared_state.dart | 368 + .../lib/common/widgets/address_book.dart | 899 ++ .../widgets/animated_rotation_widget.dart | 53 + .../lib/common/widgets/audio_input.dart | 81 + .../lib/common/widgets/autocomplete.dart | 257 + .../flutter/lib/common/widgets/chat_page.dart | 180 + .../common/widgets/connection_page_title.dart | 38 + .../lib/common/widgets/custom_password.dart | 129 + .../lib/common/widgets/custom_scale_base.dart | 156 + .../flutter/lib/common/widgets/dialog.dart | 2867 ++++ .../flutter/lib/common/widgets/gestures.dart | 797 ++ .../flutter/lib/common/widgets/login.dart | 751 + .../flutter/lib/common/widgets/my_group.dart | 309 + .../flutter/lib/common/widgets/overlay.dart | 674 + .../flutter/lib/common/widgets/peer_card.dart | 1581 +++ .../lib/common/widgets/peer_tab_page.dart | 1039 ++ .../lib/common/widgets/peers_view.dart | 598 + .../lib/common/widgets/remote_input.dart | 680 + .../lib/common/widgets/setting_widgets.dart | 340 + .../flutter/lib/common/widgets/toolbar.dart | 1024 ++ .../rustdesk-as-ref/flutter/lib/consts.dart | 685 + .../lib/desktop/pages/connection_page.dart | 615 + .../lib/desktop/pages/desktop_home_page.dart | 1146 ++ .../desktop/pages/desktop_setting_page.dart | 3124 +++++ .../lib/desktop/pages/desktop_tab_page.dart | 119 + .../lib/desktop/pages/file_manager_page.dart | 1694 +++ .../desktop/pages/file_manager_tab_page.dart | 177 + .../lib/desktop/pages/install_page.dart | 274 + .../lib/desktop/pages/port_forward_page.dart | 357 + .../desktop/pages/port_forward_tab_page.dart | 149 + .../lib/desktop/pages/remote_page.dart | 1054 ++ .../lib/desktop/pages/remote_tab_page.dart | 624 + .../lib/desktop/pages/server_page.dart | 1415 ++ .../pages/terminal_connection_manager.dart | 98 + .../lib/desktop/pages/terminal_page.dart | 205 + .../lib/desktop/pages/terminal_tab_page.dart | 591 + .../lib/desktop/pages/view_camera_page.dart | 717 + .../desktop/pages/view_camera_tab_page.dart | 522 + .../screen/desktop_file_transfer_screen.dart | 30 + .../screen/desktop_port_forward_screen.dart | 27 + .../desktop/screen/desktop_remote_screen.dart | 35 + .../screen/desktop_terminal_screen.dart | 27 + .../screen/desktop_view_camera_screen.dart | 35 + .../flutter/lib/desktop/widgets/button.dart | 171 + .../lib/desktop/widgets/dragable_divider.dart | 52 + .../widgets/kb_layout_type_chooser.dart | 225 + .../widgets/list_search_action_listener.dart | 76 + .../widgets/material_mod_popup_menu.dart | 1434 ++ .../lib/desktop/widgets/menu_button.dart | 64 + .../lib/desktop/widgets/popup_menu.dart | 755 ++ .../lib/desktop/widgets/refresh_wrapper.dart | 45 + .../lib/desktop/widgets/remote_toolbar.dart | 2801 ++++ .../lib/desktop/widgets/tabbar_widget.dart | 1540 +++ .../lib/desktop/widgets/titlebar_widget.dart | 31 + .../lib/desktop/widgets/update_progress.dart | 267 + shelled/rustdesk-as-ref/flutter/lib/main.dart | 594 + .../lib/mobile/pages/connection_page.dart | 377 + .../lib/mobile/pages/file_manager_page.dart | 769 ++ .../flutter/lib/mobile/pages/home_page.dart | 255 + .../flutter/lib/mobile/pages/remote_page.dart | 1430 ++ .../flutter/lib/mobile/pages/scan_page.dart | 165 + .../flutter/lib/mobile/pages/server_page.dart | 942 ++ .../lib/mobile/pages/settings_page.dart | 1354 ++ .../lib/mobile/pages/terminal_page.dart | 441 + .../lib/mobile/pages/view_camera_page.dart | 729 + .../mobile/widgets/custom_scale_widget.dart | 71 + .../flutter/lib/mobile/widgets/dialog.dart | 234 + .../lib/mobile/widgets/floating_mouse.dart | 1209 ++ .../widgets/floating_mouse_widgets.dart | 905 ++ .../lib/mobile/widgets/gesture_help.dart | 391 + .../flutter/lib/models/ab_model.dart | 1981 +++ .../flutter/lib/models/chat_model.dart | 562 + .../flutter/lib/models/cm_file_model.dart | 328 + .../lib/models/desktop_render_texture.dart | 256 + .../flutter/lib/models/file_model.dart | 1849 +++ .../flutter/lib/models/group_model.dart | 377 + .../flutter/lib/models/input_model.dart | 1930 +++ .../flutter/lib/models/model.dart | 4188 ++++++ .../flutter/lib/models/native_model.dart | 290 + .../flutter/lib/models/peer_model.dart | 287 + .../flutter/lib/models/peer_tab_model.dart | 270 + .../flutter/lib/models/platform_model.dart | 16 + .../flutter/lib/models/printer_model.dart | 48 + .../lib/models/relative_mouse_model.dart | 1061 ++ .../flutter/lib/models/server_model.dart | 943 ++ .../flutter/lib/models/state_model.dart | 147 + .../flutter/lib/models/terminal_model.dart | 433 + .../flutter/lib/models/user_model.dart | 246 + .../flutter/lib/models/web_model.dart | 195 + .../flutter/lib/native/common.dart | 17 + .../flutter/lib/native/custom_cursor.dart | 44 + .../flutter/lib/native/win32.dart | 41 + .../flutter/lib/plugin/common.dart | 42 + .../flutter/lib/plugin/event.dart | 18 + .../flutter/lib/plugin/handlers.dart | 79 + .../flutter/lib/plugin/manager.dart | 319 + .../flutter/lib/plugin/model.dart | 110 + .../flutter/lib/plugin/ui_manager.dart | 17 + .../flutter/lib/plugin/utils/dialogs.dart | 86 + .../flutter/lib/plugin/widgets/desc_ui.dart | 301 + .../lib/plugin/widgets/desktop_settings.dart | 202 + .../flutter/lib/utils/event_loop.dart | 79 + .../flutter/lib/utils/http_service.dart | 126 + .../flutter/lib/utils/image.dart | 133 + .../lib/utils/multi_window_manager.dart | 581 + .../flutter/lib/utils/platform_channel.dart | 45 + .../lib/utils/relative_mouse_accumulator.dart | 58 + .../flutter/lib/utils/scale.dart | 34 + .../flutter/lib/web/bridge.dart | 2038 +++ .../flutter/lib/web/common.dart | 21 + .../flutter/lib/web/custom_cursor.dart | 127 + .../flutter/lib/web/dummy.dart | 14 + .../flutter/lib/web/plugin/handlers.dart | 14 + .../flutter/lib/web/settings_page.dart | 26 + .../lib/web/texture_rgba_renderer.dart | 20 + .../flutter/lib/web/web_unique.dart | 30 + .../flutter/lib/web/win32.dart | 5 + .../rustdesk-as-ref/flutter/linux/.gitignore | 1 + .../flutter/linux/CMakeLists.txt | 209 + .../flutter/linux/bump_mouse.cc | 18 + .../flutter/linux/bump_mouse.h | 3 + .../flutter/linux/bump_mouse_x11.cc | 30 + .../flutter/linux/bump_mouse_x11.h | 3 + .../flutter/linux/flutter/CMakeLists.txt | 88 + shelled/rustdesk-as-ref/flutter/linux/main.cc | 124 + .../flutter/linux/my_application.cc | 262 + .../flutter/linux/my_application.h | 18 + .../linux/wayland_shortcuts_inhibit.cc | 244 + .../flutter/linux/wayland_shortcuts_inhibit.h | 22 + .../rustdesk-as-ref/flutter/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + shelled/rustdesk-as-ref/flutter/macos/Podfile | 40 + .../flutter/macos/Podfile.lock | 125 + .../macos/Runner.xcodeproj/project.pbxproj | 690 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../flutter/macos/Runner/AppDelegate.swift | 24 + .../flutter/macos/Runner/AppIcon.icns | Bin 0 -> 27761 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 + .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 14 + .../flutter/macos/Runner/Info.plist | 51 + .../macos/Runner/MainFlutterWindow.swift | 286 + .../flutter/macos/Runner/Release.entitlements | 14 + shelled/rustdesk-as-ref/flutter/ndk_arm.sh | 2 + shelled/rustdesk-as-ref/flutter/ndk_arm64.sh | 2 + shelled/rustdesk-as-ref/flutter/ndk_x64.sh | 2 + shelled/rustdesk-as-ref/flutter/ndk_x86.sh | 10 + shelled/rustdesk-as-ref/flutter/pubspec.lock | 1663 +++ shelled/rustdesk-as-ref/flutter/pubspec.yaml | 200 + shelled/rustdesk-as-ref/flutter/run.sh | 9 + .../rustdesk-as-ref/flutter/test/cm_test.dart | 62 + .../flutter/windows/.gitignore | 17 + .../flutter/windows/CMakeLists.txt | 122 + .../flutter/windows/flutter/CMakeLists.txt | 109 + .../flutter/windows/runner/CMakeLists.txt | 40 + .../flutter/windows/runner/Runner.rc | 121 + .../flutter/windows/runner/flutter_window.cpp | 127 + .../flutter/windows/runner/flutter_window.h | 33 + .../flutter/windows/runner/main.cpp | 168 + .../flutter/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 1961 bytes .../windows/runner/runner.exe.manifest | 20 + .../flutter/windows/runner/utils.cpp | 64 + .../flutter/windows/runner/utils.h | 19 + .../flutter/windows/runner/win32_desktop.cpp | 82 + .../flutter/windows/runner/win32_desktop.h | 13 + .../flutter/windows/runner/win32_window.cpp | 338 + .../flutter/windows/runner/win32_window.h | 99 + .../rustdesk-as-ref/libs/clipboard/Cargo.toml | 57 + .../rustdesk-as-ref/libs/clipboard/README.md | 161 + .../rustdesk-as-ref/libs/clipboard/build.rs | 35 + .../libs/clipboard/src/cliprdr.h | 247 + .../libs/clipboard/src/context_send.rs | 79 + .../rustdesk-as-ref/libs/clipboard/src/lib.rs | 298 + .../libs/clipboard/src/platform/mod.rs | 26 + .../clipboard/src/platform/unix/filetype.rs | 188 + .../clipboard/src/platform/unix/fuse/cs.rs | 1010 ++ .../clipboard/src/platform/unix/fuse/mod.rs | 225 + .../clipboard/src/platform/unix/local_file.rs | 387 + .../src/platform/unix/macos/README.md | 25 + .../platform/unix/macos/item_data_provider.rs | 77 + .../clipboard/src/platform/unix/macos/mod.rs | 14 + .../src/platform/unix/macos/paste_observer.rs | 179 + .../src/platform/unix/macos/paste_task.rs | 639 + .../platform/unix/macos/pasteboard_context.rs | 460 + .../libs/clipboard/src/platform/unix/mod.rs | 58 + .../clipboard/src/platform/unix/serv_files.rs | 271 + .../libs/clipboard/src/platform/windows.rs | 1327 ++ .../libs/clipboard/src/windows/wf_cliprdr.c | 3380 +++++ .../rustdesk-as-ref/libs/enigo/.gitattributes | 1 + .../.github/ISSUE_TEMPLATE/bug_report.md | 25 + .../.github/ISSUE_TEMPLATE/feature_request.md | 20 + .../enigo/.github/ISSUE_TEMPLATE/question.md | 19 + shelled/rustdesk-as-ref/libs/enigo/.gitignore | 14 + .../rustdesk-as-ref/libs/enigo/.travis.yml | 15 + shelled/rustdesk-as-ref/libs/enigo/Cargo.toml | 44 + shelled/rustdesk-as-ref/libs/enigo/LICENSE | 21 + shelled/rustdesk-as-ref/libs/enigo/README.md | 46 + .../rustdesk-as-ref/libs/enigo/appveyor.yml | 121 + shelled/rustdesk-as-ref/libs/enigo/build.rs | 61 + .../libs/enigo/examples/dsl.rs | 11 + .../libs/enigo/examples/key.rs | 17 + .../libs/enigo/examples/keyboard.rs | 16 + .../libs/enigo/examples/mouse.rs | 54 + .../libs/enigo/examples/timer.rs | 22 + .../rustdesk-as-ref/libs/enigo/rustfmt.toml | 1 + shelled/rustdesk-as-ref/libs/enigo/src/dsl.rs | 184 + shelled/rustdesk-as-ref/libs/enigo/src/lib.rs | 552 + .../libs/enigo/src/linux/mod.rs | 4 + .../libs/enigo/src/linux/nix_impl.rs | 392 + .../libs/enigo/src/linux/xdo.rs | 412 + .../libs/enigo/src/macos/keycodes.rs | 120 + .../libs/enigo/src/macos/macos_impl.rs | 864 ++ .../libs/enigo/src/macos/mod.rs | 4 + .../libs/enigo/src/win/keycodes.rs | 83 + .../rustdesk-as-ref/libs/enigo/src/win/mod.rs | 4 + .../libs/enigo/src/win/win_impl.rs | 478 + .../libs/libxdo-sys-stub/Cargo.toml | 9 + .../libs/libxdo-sys-stub/src/lib.rs | 505 + .../rustdesk-as-ref/libs/portable/.gitignore | 3 + .../rustdesk-as-ref/libs/portable/Cargo.lock | 285 + .../rustdesk-as-ref/libs/portable/Cargo.toml | 39 + .../rustdesk-as-ref/libs/portable/build.rs | 20 + .../rustdesk-as-ref/libs/portable/generate.py | 108 + .../libs/portable/requirements.txt | 1 + .../libs/portable/src/bin_reader.rs | 139 + .../rustdesk-as-ref/libs/portable/src/main.rs | 248 + .../libs/portable/src/res/spin.gif | Bin 0 -> 59332 bytes .../rustdesk-as-ref/libs/portable/src/ui.rs | 232 + .../libs/remote_printer/Cargo.toml | 11 + .../libs/remote_printer/src/lib.rs | 34 + .../libs/remote_printer/src/setup/driver.rs | 202 + .../libs/remote_printer/src/setup/mod.rs | 101 + .../libs/remote_printer/src/setup/port.rs | 128 + .../libs/remote_printer/src/setup/printer.rs | 161 + .../libs/remote_printer/src/setup/setup.rs | 94 + shelled/rustdesk-as-ref/libs/scrap/.gitignore | 4 + shelled/rustdesk-as-ref/libs/scrap/Cargo.toml | 68 + shelled/rustdesk-as-ref/libs/scrap/README.md | 62 + shelled/rustdesk-as-ref/libs/scrap/build.rs | 267 + .../libs/scrap/examples/benchmark.rs | 331 + .../libs/scrap/examples/capture_mag.rs | 124 + .../libs/scrap/examples/ffplay.rs | 58 + .../libs/scrap/examples/list.rs | 16 + .../libs/scrap/examples/record-screen.rs | 160 + .../libs/scrap/examples/screenshot.rs | 141 + .../libs/scrap/src/android/ffi.rs | 511 + .../libs/scrap/src/android/mod.rs | 3 + .../libs/scrap/src/bindings/aom_ffi.h | 10 + .../libs/scrap/src/bindings/vpx_ffi.h | 9 + .../libs/scrap/src/bindings/yuv_ffi.h | 6 + .../libs/scrap/src/common/android.rs | 189 + .../libs/scrap/src/common/aom.rs | 581 + .../libs/scrap/src/common/camera.rs | 286 + .../libs/scrap/src/common/codec.rs | 1157 ++ .../libs/scrap/src/common/convert.rs | 236 + .../libs/scrap/src/common/dxgi.rs | 264 + .../libs/scrap/src/common/hwcodec.rs | 763 ++ .../libs/scrap/src/common/linux.rs | 139 + .../libs/scrap/src/common/mediacodec.rs | 171 + .../libs/scrap/src/common/mod.rs | 547 + .../libs/scrap/src/common/quartz.rs | 151 + .../libs/scrap/src/common/record.rs | 423 + .../libs/scrap/src/common/vpx.rs | 26 + .../libs/scrap/src/common/vpxcodec.rs | 597 + .../libs/scrap/src/common/vram.rs | 404 + .../libs/scrap/src/common/wayland.rs | 129 + .../libs/scrap/src/common/x11.rs | 139 + .../libs/scrap/src/dxgi/gdi.rs | 213 + .../libs/scrap/src/dxgi/mag.rs | 651 + .../libs/scrap/src/dxgi/mod.rs | 884 ++ shelled/rustdesk-as-ref/libs/scrap/src/lib.rs | 26 + .../libs/scrap/src/quartz/capturer.rs | 111 + .../libs/scrap/src/quartz/config.rs | 75 + .../libs/scrap/src/quartz/display.rs | 87 + .../libs/scrap/src/quartz/ffi.rs | 241 + .../libs/scrap/src/quartz/frame.rs | 71 + .../libs/scrap/src/quartz/mod.rs | 17 + .../rustdesk-as-ref/libs/scrap/src/wayland.rs | 6 + .../libs/scrap/src/wayland/README.md | 11 + .../libs/scrap/src/wayland/capturable.rs | 60 + .../libs/scrap/src/wayland/display.rs | 256 + .../libs/scrap/src/wayland/pipewire.rs | 1528 +++ .../src/wayland/remote_desktop_portal.rs | 315 + .../libs/scrap/src/wayland/request_portal.rs | 45 + .../scrap/src/wayland/screencast_portal.rs | 106 + .../libs/scrap/src/x11/capturer.rs | 115 + .../libs/scrap/src/x11/display.rs | 70 + .../rustdesk-as-ref/libs/scrap/src/x11/ffi.rs | 283 + .../libs/scrap/src/x11/iter.rs | 138 + .../rustdesk-as-ref/libs/scrap/src/x11/mod.rs | 10 + .../libs/scrap/src/x11/server.rs | 146 + .../libs/virtual_display/Cargo.lock | 1358 ++ .../libs/virtual_display/Cargo.toml | 10 + .../libs/virtual_display/README.md | 3 + .../libs/virtual_display/dylib/Cargo.toml | 19 + .../libs/virtual_display/dylib/README.md | 32 + .../libs/virtual_display/dylib/build.rs | 35 + .../dylib/examples/idd_controller.rs | 188 + .../libs/virtual_display/dylib/src/lib.rs | 191 + .../dylib/src/win10/IddController.c | 1006 ++ .../dylib/src/win10/IddController.h | 161 + .../virtual_display/dylib/src/win10/Public.h | 54 + .../virtual_display/dylib/src/win10/idd.rs | 215 + .../virtual_display/dylib/src/win10/mod.rs | 9 + .../examples/virtual_display_1.rs | 102 + .../libs/virtual_display/src/lib.rs | 196 + shelled/rustdesk-as-ref/res/DEBIAN/postinst | 27 + shelled/rustdesk-as-ref/res/DEBIAN/postrm | 11 + shelled/rustdesk-as-ref/res/DEBIAN/preinst | 16 + shelled/rustdesk-as-ref/res/DEBIAN/prerm | 27 + shelled/rustdesk-as-ref/res/PKGBUILD | 35 + shelled/rustdesk-as-ref/res/ab.py | 791 ++ shelled/rustdesk-as-ref/res/audits.py | 374 + shelled/rustdesk-as-ref/res/bump.sh | 3 + shelled/rustdesk-as-ref/res/device-groups.py | 274 + shelled/rustdesk-as-ref/res/devices.py | 205 + .../patches/0000-flutter-android-x86.patch | 16 + .../patches/0001-x86-no-debuggable.patch | 24 + shelled/rustdesk-as-ref/res/gen_icon.sh | 8 + shelled/rustdesk-as-ref/res/icon.ico | Bin 0 -> 99678 bytes shelled/rustdesk-as-ref/res/inline-sciter.py | 82 + shelled/rustdesk-as-ref/res/job.py | 321 + shelled/rustdesk-as-ref/res/lang.py | 90 + shelled/rustdesk-as-ref/res/manifest.xml | 36 + shelled/rustdesk-as-ref/res/msi/.gitignore | 13 + .../res/msi/CustomActions/Common.h | 23 + .../res/msi/CustomActions/CustomActions.cpp | 1080 ++ .../res/msi/CustomActions/CustomActions.def | 16 + .../msi/CustomActions/CustomActions.vcxproj | 86 + .../res/msi/CustomActions/DeviceUtils.cpp | 84 + .../res/msi/CustomActions/FirewallRules.cpp | 413 + .../res/msi/CustomActions/ReadConfig.cpp | 36 + .../res/msi/CustomActions/RemotePrinter.cpp | 517 + .../res/msi/CustomActions/ServiceUtils.cpp | 175 + .../res/msi/CustomActions/dllmain.cpp | 26 + .../res/msi/CustomActions/framework.h | 10 + .../res/msi/CustomActions/packages.config | 5 + .../res/msi/CustomActions/pch.cpp | 5 + .../res/msi/CustomActions/pch.h | 13 + .../res/msi/Package/Components/Folders.wxs | 38 + .../res/msi/Package/Components/Regs.wxs | 56 + .../res/msi/Package/Components/RustDesk.wxs | 154 + .../Package/Fragments/AddRemoveProperties.wxs | 37 + .../msi/Package/Fragments/CustomActions.wxs | 23 + .../Package/Fragments/ShortcutProperties.wxs | 83 + .../res/msi/Package/Fragments/Upgrades.wxs | 10 + .../res/msi/Package/Includes.wxi | 7 + .../msi/Package/Language/Package.en-us.wxl | 56 + .../res/msi/Package/Language/WixExt_en-us.wxl | 32 + .../res/msi/Package/License.rtf | 303 + .../res/msi/Package/Package.wixproj | 22 + .../res/msi/Package/Package.wxs | 62 + .../res/msi/Package/UI/AnotherApp.wxs | 15 + .../res/msi/Package/UI/MyInstallDirDlg.wxs | 32 + .../res/msi/Package/UI/MyInstallDlg.wxs | 87 + shelled/rustdesk-as-ref/res/msi/README.md | 44 + shelled/rustdesk-as-ref/res/msi/msi.sln | 26 + shelled/rustdesk-as-ref/res/msi/preprocess.py | 560 + shelled/rustdesk-as-ref/res/osx-dist.sh | 14 + shelled/rustdesk-as-ref/res/pacman_install | 47 + .../rustdesk-as-ref/res/pam.d/rustdesk.debian | 5 + .../rustdesk-as-ref/res/pam.d/rustdesk.suse | 5 + .../rustdesk-as-ref/res/rpm-flutter-suse.spec | 98 + shelled/rustdesk-as-ref/res/rpm-flutter.spec | 98 + shelled/rustdesk-as-ref/res/rpm-suse.spec | 93 + shelled/rustdesk-as-ref/res/rpm.spec | 96 + .../rustdesk-as-ref/res/rustdesk-link.desktop | 11 + shelled/rustdesk-as-ref/res/rustdesk.desktop | 19 + shelled/rustdesk-as-ref/res/rustdesk.service | 22 + shelled/rustdesk-as-ref/res/startwm.sh | 130 + shelled/rustdesk-as-ref/res/strategies.py | 301 + shelled/rustdesk-as-ref/res/tray-icon.ico | Bin 0 -> 4286 bytes shelled/rustdesk-as-ref/res/user-groups.py | 302 + shelled/rustdesk-as-ref/res/users.py | 292 + .../res/vcpkg/aom/aom-avx2.diff | 60 + .../res/vcpkg/aom/aom-install.diff | 75 + .../vcpkg/aom/aom-uninitialized-pointer.diff | 13 + .../res/vcpkg/aom/portfile.cmake | 79 + .../rustdesk-as-ref/res/vcpkg/aom/vcpkg.json | 18 + .../ffmpeg/0001-create-lib-libraries.patch | 27 + .../res/vcpkg/ffmpeg/0002-fix-msvc-link.patch | 11 + .../ffmpeg/0003-fix-windowsinclude.patch | 13 + .../res/vcpkg/ffmpeg/0004-dependencies.patch | 65 + .../res/vcpkg/ffmpeg/0005-fix-nasm.patch | 78 + .../vcpkg/ffmpeg/0007-fix-lib-naming.patch | 12 + .../res/vcpkg/ffmpeg/0013-define-WINVER.patch | 15 + .../ffmpeg/0020-fix-aarch64-libswscale.patch | 28 + .../vcpkg/ffmpeg/0024-fix-osx-host-c11.patch | 15 + ...av_stream_get_first_dts-for-chromium.patch | 35 + ...0041-add-const-for-opengl-definition.patch | 13 + .../vcpkg/ffmpeg/0042-fix-arm64-linux.patch | 9 + .../res/vcpkg/ffmpeg/0043-fix-miss-head.patch | 12 + .../res/vcpkg/ffmpeg/build.sh.in | 152 + ...dd-query_timeout-option-for-h264-hev.patch | 71 + ...-amfenc-reconfig-when-bitrate-change.patch | 71 + .../0004-videotoolbox-changing-bitrate.patch | 85 + .../0005-mediacodec-changing-bitrate.patch | 246 + .../ffmpeg/patch/0006-dlopen-libva.patch | 1883 +++ .../patch/0007-fix-linux-configure.patch | 30 + .../patch/0008-remove-amf-loop-query.patch | 26 + .../0009-fix-nvenc-reconfigure-blur.patch | 28 + ...10.disable-loading-DLLs-from-app-dir.patch | 31 + ...1-android-mediacodec-encode-align-64.patch | 42 + ...acos-big-sur-CVBufferCopyAttachments.patch | 60 + .../res/vcpkg/ffmpeg/portfile.cmake | 705 + .../vcpkg/ffmpeg/vcpkg-cmake-wrapper.cmake | 47 + .../res/vcpkg/ffmpeg/vcpkg.json | 44 + .../0003-add-uwp-v142-and-v143-support.patch | 168 + .../libvpx/0004-remove-library-suffixes.patch | 13 + .../res/vcpkg/libvpx/portfile.cmake | 316 + .../libvpx/unofficial-libvpx-config.cmake.in | 49 + .../res/vcpkg/libvpx/vcpkg.json | 26 + .../res/vcpkg/libvpx/vpx.pc.in | 12 + .../res/vcpkg/libyuv/fix-cmakelists.patch | 80 + .../res/vcpkg/libyuv/libyuv-config.cmake | 5 + .../res/vcpkg/libyuv/portfile.cmake | 81 + .../rustdesk-as-ref/res/vcpkg/libyuv/usage | 4 + .../res/vcpkg/libyuv/usage-msvc | 9 + .../res/vcpkg/libyuv/vcpkg.json | 22 + .../0003-upgrade-cmake-3.14.patch | 10 + .../res/vcpkg/mfx-dispatch/fix-pkgconf.patch | 39 + .../mfx-dispatch/fix-unresolved-symbol.patch | 66 + .../res/vcpkg/mfx-dispatch/portfile.cmake | 40 + .../res/vcpkg/mfx-dispatch/vcpkg.json | 16 + .../vcpkg/opus/fix-pkgconfig-version.patch | 15 + .../res/vcpkg/opus/portfile.cmake | 61 + .../rustdesk-as-ref/res/vcpkg/opus/vcpkg.json | 22 + shelled/rustdesk-as-ref/res/xorg.conf | 30 + shelled/rustdesk-as-ref/src/auth_2fa.rs | 204 + shelled/rustdesk-as-ref/src/cli.rs | 193 + shelled/rustdesk-as-ref/src/client.rs | 4198 ++++++ .../rustdesk-as-ref/src/client/file_trait.rs | 193 + shelled/rustdesk-as-ref/src/client/helper.rs | 37 + shelled/rustdesk-as-ref/src/client/io_loop.rs | 2441 ++++ .../rustdesk-as-ref/src/client/screenshot.rs | 99 + shelled/rustdesk-as-ref/src/clipboard.rs | 885 ++ shelled/rustdesk-as-ref/src/clipboard_file.rs | 427 + shelled/rustdesk-as-ref/src/common.rs | 3007 ++++ shelled/rustdesk-as-ref/src/core_main.rs | 850 ++ shelled/rustdesk-as-ref/src/custom_server.rs | 219 + shelled/rustdesk-as-ref/src/flutter.rs | 2363 ++++ shelled/rustdesk-as-ref/src/flutter_ffi.rs | 3082 +++++ shelled/rustdesk-as-ref/src/hbbs_http.rs | 40 + .../rustdesk-as-ref/src/hbbs_http/account.rs | 366 + .../src/hbbs_http/downloader.rs | 309 + .../src/hbbs_http/http_client.rs | 336 + .../src/hbbs_http/record_upload.rs | 211 + shelled/rustdesk-as-ref/src/hbbs_http/sync.rs | 310 + shelled/rustdesk-as-ref/src/ipc.rs | 1700 +++ shelled/rustdesk-as-ref/src/kcp_stream.rs | 151 + shelled/rustdesk-as-ref/src/keyboard.rs | 1401 ++ shelled/rustdesk-as-ref/src/lan.rs | 344 + shelled/rustdesk-as-ref/src/lang.rs | 269 + shelled/rustdesk-as-ref/src/lang/README.md | 4 + shelled/rustdesk-as-ref/src/lang/ar.rs | 747 + shelled/rustdesk-as-ref/src/lang/be.rs | 747 + shelled/rustdesk-as-ref/src/lang/bg.rs | 747 + shelled/rustdesk-as-ref/src/lang/ca.rs | 747 + shelled/rustdesk-as-ref/src/lang/cn.rs | 747 + shelled/rustdesk-as-ref/src/lang/cs.rs | 747 + shelled/rustdesk-as-ref/src/lang/da.rs | 747 + shelled/rustdesk-as-ref/src/lang/de.rs | 747 + shelled/rustdesk-as-ref/src/lang/el.rs | 747 + shelled/rustdesk-as-ref/src/lang/en.rs | 278 + shelled/rustdesk-as-ref/src/lang/eo.rs | 747 + shelled/rustdesk-as-ref/src/lang/es.rs | 747 + shelled/rustdesk-as-ref/src/lang/et.rs | 747 + shelled/rustdesk-as-ref/src/lang/eu.rs | 747 + shelled/rustdesk-as-ref/src/lang/fa.rs | 747 + shelled/rustdesk-as-ref/src/lang/fi.rs | 747 + shelled/rustdesk-as-ref/src/lang/fr.rs | 747 + shelled/rustdesk-as-ref/src/lang/ge.rs | 747 + shelled/rustdesk-as-ref/src/lang/he.rs | 747 + shelled/rustdesk-as-ref/src/lang/hr.rs | 747 + shelled/rustdesk-as-ref/src/lang/hu.rs | 747 + shelled/rustdesk-as-ref/src/lang/id.rs | 747 + shelled/rustdesk-as-ref/src/lang/it.rs | 747 + shelled/rustdesk-as-ref/src/lang/ja.rs | 747 + shelled/rustdesk-as-ref/src/lang/ko.rs | 747 + shelled/rustdesk-as-ref/src/lang/kz.rs | 747 + shelled/rustdesk-as-ref/src/lang/lt.rs | 747 + shelled/rustdesk-as-ref/src/lang/lv.rs | 747 + shelled/rustdesk-as-ref/src/lang/nb.rs | 747 + shelled/rustdesk-as-ref/src/lang/nl.rs | 747 + shelled/rustdesk-as-ref/src/lang/pl.rs | 747 + shelled/rustdesk-as-ref/src/lang/pt_PT.rs | 747 + shelled/rustdesk-as-ref/src/lang/ptbr.rs | 747 + shelled/rustdesk-as-ref/src/lang/ro.rs | 747 + shelled/rustdesk-as-ref/src/lang/ru.rs | 747 + shelled/rustdesk-as-ref/src/lang/sc.rs | 747 + shelled/rustdesk-as-ref/src/lang/sk.rs | 747 + shelled/rustdesk-as-ref/src/lang/sl.rs | 747 + shelled/rustdesk-as-ref/src/lang/sq.rs | 747 + shelled/rustdesk-as-ref/src/lang/sr.rs | 747 + shelled/rustdesk-as-ref/src/lang/sv.rs | 747 + shelled/rustdesk-as-ref/src/lang/ta.rs | 747 + shelled/rustdesk-as-ref/src/lang/template.rs | 747 + shelled/rustdesk-as-ref/src/lang/th.rs | 747 + shelled/rustdesk-as-ref/src/lang/tr.rs | 747 + shelled/rustdesk-as-ref/src/lang/tw.rs | 747 + shelled/rustdesk-as-ref/src/lang/uk.rs | 747 + shelled/rustdesk-as-ref/src/lang/vi.rs | 747 + shelled/rustdesk-as-ref/src/lib.rs | 79 + shelled/rustdesk-as-ref/src/main.rs | 104 + shelled/rustdesk-as-ref/src/naming.rs | 28 + .../rustdesk-as-ref/src/platform/delegate.rs | 277 + .../rustdesk-as-ref/src/platform/gtk_sudo.rs | 773 ++ shelled/rustdesk-as-ref/src/platform/linux.rs | 2209 +++ .../src/platform/linux_desktop_manager.rs | 744 + shelled/rustdesk-as-ref/src/platform/macos.mm | 909 ++ shelled/rustdesk-as-ref/src/platform/macos.rs | 1230 ++ shelled/rustdesk-as-ref/src/platform/mod.rs | 248 + .../platform/privileges_scripts/agent.plist | 37 + .../platform/privileges_scripts/daemon.plist | 30 + .../platform/privileges_scripts/install.scpt | 16 + .../privileges_scripts/uninstall.scpt | 6 + .../platform/privileges_scripts/update.scpt | 26 + .../src/platform/win_device.rs | 459 + .../rustdesk-as-ref/src/platform/windows.cc | 1058 ++ .../rustdesk-as-ref/src/platform/windows.rs | 4384 ++++++ .../src/platform/windows_delete_test_cert.cc | 406 + .../src/plugin/callback_ext.rs | 44 + .../src/plugin/callback_msg.rs | 411 + shelled/rustdesk-as-ref/src/plugin/config.rs | 363 + shelled/rustdesk-as-ref/src/plugin/desc.rs | 100 + shelled/rustdesk-as-ref/src/plugin/errno.rs | 50 + shelled/rustdesk-as-ref/src/plugin/ipc.rs | 230 + shelled/rustdesk-as-ref/src/plugin/manager.rs | 600 + shelled/rustdesk-as-ref/src/plugin/mod.rs | 188 + shelled/rustdesk-as-ref/src/plugin/native.rs | 40 + .../src/plugin/native_handlers/macros.rs | 27 + .../src/plugin/native_handlers/mod.rs | 126 + .../src/plugin/native_handlers/session.rs | 219 + .../src/plugin/native_handlers/ui.rs | 143 + shelled/rustdesk-as-ref/src/plugin/plog.rs | 34 + shelled/rustdesk-as-ref/src/plugin/plugins.rs | 659 + shelled/rustdesk-as-ref/src/port_forward.rs | 220 + shelled/rustdesk-as-ref/src/privacy_mode.rs | 431 + .../rustdesk-as-ref/src/privacy_mode/macos.rs | 81 + .../privacy_mode/win_exclude_from_capture.rs | 11 + .../src/privacy_mode/win_input.rs | 276 + .../src/privacy_mode/win_mag.rs | 57 + .../src/privacy_mode/win_topmost_window.rs | 383 + .../src/privacy_mode/win_virtual_display.rs | 586 + .../src/rendezvous_mediator.rs | 933 ++ shelled/rustdesk-as-ref/src/server.rs | 834 ++ .../src/server/audio_service.rs | 527 + .../src/server/clipboard_service.rs | 274 + .../rustdesk-as-ref/src/server/connection.rs | 5671 ++++++++ shelled/rustdesk-as-ref/src/server/dbus.rs | 92 + .../src/server/display_service.rs | 488 + .../src/server/input_service.rs | 2373 ++++ .../src/server/portable_service.rs | 992 ++ .../src/server/printer_service.rs | 163 + .../rustdesk-as-ref/src/server/rdp_input.rs | 605 + shelled/rustdesk-as-ref/src/server/service.rs | 358 + .../src/server/terminal_helper.rs | 1062 ++ .../src/server/terminal_service.rs | 1847 +++ shelled/rustdesk-as-ref/src/server/uinput.rs | 1307 ++ .../rustdesk-as-ref/src/server/video_qos.rs | 595 + .../src/server/video_service.rs | 1419 ++ shelled/rustdesk-as-ref/src/server/wayland.rs | 308 + shelled/rustdesk-as-ref/src/service.rs | 11 + shelled/rustdesk-as-ref/src/tray.rs | 281 + shelled/rustdesk-as-ref/src/ui.rs | 872 ++ shelled/rustdesk-as-ref/src/ui/ab.tis | 772 ++ shelled/rustdesk-as-ref/src/ui/chatbox.html | 35 + shelled/rustdesk-as-ref/src/ui/cm.css | 279 + shelled/rustdesk-as-ref/src/ui/cm.html | 21 + shelled/rustdesk-as-ref/src/ui/cm.rs | 186 + shelled/rustdesk-as-ref/src/ui/cm.tis | 569 + shelled/rustdesk-as-ref/src/ui/common.css | 492 + shelled/rustdesk-as-ref/src/ui/common.tis | 482 + .../rustdesk-as-ref/src/ui/file_transfer.css | 269 + .../rustdesk-as-ref/src/ui/file_transfer.tis | 819 ++ shelled/rustdesk-as-ref/src/ui/grid.tis | 258 + shelled/rustdesk-as-ref/src/ui/header.css | 97 + shelled/rustdesk-as-ref/src/ui/header.tis | 716 + shelled/rustdesk-as-ref/src/ui/index.css | 441 + shelled/rustdesk-as-ref/src/ui/index.html | 19 + shelled/rustdesk-as-ref/src/ui/index.tis | 1680 +++ shelled/rustdesk-as-ref/src/ui/install.html | 22 + shelled/rustdesk-as-ref/src/ui/install.tis | 70 + shelled/rustdesk-as-ref/src/ui/msgbox.tis | 390 + .../rustdesk-as-ref/src/ui/port_forward.tis | 77 + shelled/rustdesk-as-ref/src/ui/printer.tis | 41 + shelled/rustdesk-as-ref/src/ui/remote.css | 46 + shelled/rustdesk-as-ref/src/ui/remote.html | 44 + shelled/rustdesk-as-ref/src/ui/remote.rs | 918 ++ shelled/rustdesk-as-ref/src/ui/remote.tis | 599 + .../rustdesk-as-ref/src/ui_cm_interface.rs | 1853 +++ shelled/rustdesk-as-ref/src/ui_interface.rs | 1611 +++ .../src/ui_session_interface.rs | 2055 +++ shelled/rustdesk-as-ref/src/updater.rs | 290 + .../src/virtual_display_manager.rs | 925 ++ .../rustdesk-as-ref/src/whiteboard/client.rs | 258 + .../rustdesk-as-ref/src/whiteboard/linux.rs | 463 + .../rustdesk-as-ref/src/whiteboard/macos.rs | 323 + shelled/rustdesk-as-ref/src/whiteboard/mod.rs | 41 + .../rustdesk-as-ref/src/whiteboard/server.rs | 171 + .../src/whiteboard/win_linux.rs | 180 + .../rustdesk-as-ref/src/whiteboard/windows.rs | 230 + shelled/rustdesk-as-ref/vcpkg.json | 105 + shelled/shelled-os-ui/.gitignore | 4 +- shelled/shelled-os-ui/package-lock.json | 4 +- shelled/shelled-os-ui/src-tauri/Cargo.lock | 4946 +++++++ .../src-tauri/gen/schemas/acl-manifests.json | 1 + .../src-tauri/gen/schemas/capabilities.json | 1 + .../src-tauri/gen/schemas/desktop-schema.json | 2244 +++ .../src-tauri/gen/schemas/windows-schema.json | 2244 +++ 801 files changed, 274773 insertions(+), 3 deletions(-) rename shelled/{plan.md => plan-os-ui.md} (100%) create mode 100644 shelled/rustdesk-as-ref/.cargo/config.toml create mode 100644 shelled/rustdesk-as-ref/.gitattributes create mode 100644 shelled/rustdesk-as-ref/.github/FUNDING.yml create mode 100644 shelled/rustdesk-as-ref/.github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 shelled/rustdesk-as-ref/.github/ISSUE_TEMPLATE/config.yml create mode 100644 shelled/rustdesk-as-ref/.github/dependabot.yml create mode 100644 shelled/rustdesk-as-ref/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff create mode 100644 shelled/rustdesk-as-ref/.github/workflows/bridge.yml create mode 100644 shelled/rustdesk-as-ref/.github/workflows/ci.yml create mode 100644 shelled/rustdesk-as-ref/.github/workflows/clear-cache.yml create mode 100644 shelled/rustdesk-as-ref/.github/workflows/fdroid.yml create mode 100644 shelled/rustdesk-as-ref/.github/workflows/flutter-build.yml create mode 100644 shelled/rustdesk-as-ref/.github/workflows/flutter-ci.yml create mode 100644 shelled/rustdesk-as-ref/.github/workflows/flutter-nightly.yml create mode 100644 shelled/rustdesk-as-ref/.github/workflows/flutter-tag.yml create mode 100644 shelled/rustdesk-as-ref/.github/workflows/playground.yml create mode 100644 shelled/rustdesk-as-ref/.github/workflows/third-party-RustDeskTempTopMostWindow.yml create mode 100644 shelled/rustdesk-as-ref/.gitignore create mode 100644 shelled/rustdesk-as-ref/.gitmodules create mode 100644 shelled/rustdesk-as-ref/CLAUDE.md create mode 100644 shelled/rustdesk-as-ref/Cargo.lock create mode 100644 shelled/rustdesk-as-ref/Cargo.toml create mode 100644 shelled/rustdesk-as-ref/Dockerfile create mode 100644 shelled/rustdesk-as-ref/LICENCE create mode 100644 shelled/rustdesk-as-ref/README.md create mode 100644 shelled/rustdesk-as-ref/appimage/AppImageBuilder-aarch64.yml create mode 100644 shelled/rustdesk-as-ref/appimage/AppImageBuilder-x86_64.yml create mode 100644 shelled/rustdesk-as-ref/build.py create mode 100644 shelled/rustdesk-as-ref/build.rs create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-DE.md create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-JP.md create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-KR.md create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-NL.md create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-NO.md create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-PL.md create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-RO.md create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-RU.md create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-TR.md create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-ZH.md create mode 100644 shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-DE.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-ID.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-IT.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-JP.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-KR.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-NL.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-NO.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-PL.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-RO.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-RU.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-TR.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING-ZH.md create mode 100644 shelled/rustdesk-as-ref/docs/CONTRIBUTING.md create mode 100644 shelled/rustdesk-as-ref/docs/README-AR.md create mode 100644 shelled/rustdesk-as-ref/docs/README-CS.md create mode 100644 shelled/rustdesk-as-ref/docs/README-DA.md create mode 100644 shelled/rustdesk-as-ref/docs/README-DE.md create mode 100644 shelled/rustdesk-as-ref/docs/README-EO.md create mode 100644 shelled/rustdesk-as-ref/docs/README-ES.md create mode 100644 shelled/rustdesk-as-ref/docs/README-FA.md create mode 100644 shelled/rustdesk-as-ref/docs/README-FI.md create mode 100644 shelled/rustdesk-as-ref/docs/README-FR.md create mode 100644 shelled/rustdesk-as-ref/docs/README-GR.md create mode 100644 shelled/rustdesk-as-ref/docs/README-HU.md create mode 100644 shelled/rustdesk-as-ref/docs/README-ID.md create mode 100644 shelled/rustdesk-as-ref/docs/README-IT.md create mode 100644 shelled/rustdesk-as-ref/docs/README-JP.md create mode 100644 shelled/rustdesk-as-ref/docs/README-KR.md create mode 100644 shelled/rustdesk-as-ref/docs/README-ML.md create mode 100644 shelled/rustdesk-as-ref/docs/README-NL.md create mode 100644 shelled/rustdesk-as-ref/docs/README-NO.md create mode 100644 shelled/rustdesk-as-ref/docs/README-PL.md create mode 100644 shelled/rustdesk-as-ref/docs/README-PTBR.md create mode 100644 shelled/rustdesk-as-ref/docs/README-RO.md create mode 100644 shelled/rustdesk-as-ref/docs/README-RU.md create mode 100644 shelled/rustdesk-as-ref/docs/README-TR.md create mode 100644 shelled/rustdesk-as-ref/docs/README-UA.md create mode 100644 shelled/rustdesk-as-ref/docs/README-VN.md create mode 100644 shelled/rustdesk-as-ref/docs/README-ZH.md create mode 100644 shelled/rustdesk-as-ref/docs/SECURITY-DE.md create mode 100644 shelled/rustdesk-as-ref/docs/SECURITY-IT.md create mode 100644 shelled/rustdesk-as-ref/docs/SECURITY-JP.md create mode 100644 shelled/rustdesk-as-ref/docs/SECURITY-KR.md create mode 100644 shelled/rustdesk-as-ref/docs/SECURITY-NL.md create mode 100644 shelled/rustdesk-as-ref/docs/SECURITY-NO.md create mode 100644 shelled/rustdesk-as-ref/docs/SECURITY-PL.md create mode 100644 shelled/rustdesk-as-ref/docs/SECURITY-RO.md create mode 100644 shelled/rustdesk-as-ref/docs/SECURITY-TR.md create mode 100644 shelled/rustdesk-as-ref/docs/SECURITY.md create mode 100644 shelled/rustdesk-as-ref/entrypoint.sh create mode 100644 shelled/rustdesk-as-ref/examples/ipc.rs create mode 100644 shelled/rustdesk-as-ref/fastlane/metadata/android/en-US/full_description.txt create mode 100644 shelled/rustdesk-as-ref/fastlane/metadata/android/en-US/short_description.txt create mode 100644 shelled/rustdesk-as-ref/fastlane/metadata/android/fr-FR/full_description.txt create mode 100644 shelled/rustdesk-as-ref/fastlane/metadata/android/fr-FR/short_description.txt create mode 100644 shelled/rustdesk-as-ref/fastlane/metadata/android/nl-NL/full_description.txt create mode 100644 shelled/rustdesk-as-ref/fastlane/metadata/android/nl-NL/short_description.txt create mode 100644 shelled/rustdesk-as-ref/fastlane/metadata/android/zh-CN/full_description.txt create mode 100644 shelled/rustdesk-as-ref/fastlane/metadata/android/zh-CN/short_description.txt create mode 100644 shelled/rustdesk-as-ref/flatpak/com.rustdesk.RustDesk.metainfo.xml create mode 100644 shelled/rustdesk-as-ref/flatpak/rustdesk.json create mode 100644 shelled/rustdesk-as-ref/flutter/.gitattributes create mode 100644 shelled/rustdesk-as-ref/flutter/.gitignore create mode 100644 shelled/rustdesk-as-ref/flutter/.metadata create mode 100644 shelled/rustdesk-as-ref/flutter/README.md create mode 100644 shelled/rustdesk-as-ref/flutter/analysis_options.yaml create mode 100644 shelled/rustdesk-as-ref/flutter/android/.gitignore create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/build.gradle create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/proguard-rules create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/debug/AndroidManifest.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/AndroidManifest.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainApplication.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/VolumeController.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/ffi.kt create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/check_blue.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/close_red.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/floating_window.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/launch_background.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values-night/styles.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/colors.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/ic_launcher_background.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/strings.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/styles.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/main/res/xml/accessibility_service_config.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/app/src/profile/AndroidManifest.xml create mode 100644 shelled/rustdesk-as-ref/flutter/android/build.gradle create mode 100644 shelled/rustdesk-as-ref/flutter/android/gradle.properties create mode 100644 shelled/rustdesk-as-ref/flutter/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 shelled/rustdesk-as-ref/flutter/android/settings.gradle create mode 100644 shelled/rustdesk-as-ref/flutter/assets/address_book.ttf create mode 100644 shelled/rustdesk-as-ref/flutter/assets/device_group.ttf create mode 100644 shelled/rustdesk-as-ref/flutter/assets/gestures.ttf create mode 100644 shelled/rustdesk-as-ref/flutter/assets/more.ttf create mode 100644 shelled/rustdesk-as-ref/flutter/assets/peer_searchbar.ttf create mode 100644 shelled/rustdesk-as-ref/flutter/assets/tabbar.ttf create mode 100644 shelled/rustdesk-as-ref/flutter/build_android.sh create mode 100644 shelled/rustdesk-as-ref/flutter/build_android_deps.sh create mode 100644 shelled/rustdesk-as-ref/flutter/build_fdroid.sh create mode 100644 shelled/rustdesk-as-ref/flutter/build_ios.sh create mode 100644 shelled/rustdesk-as-ref/flutter/ios/.gitignore create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Flutter/AppFrameworkInfo.plist create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Flutter/Debug.xcconfig create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Flutter/Release.xcconfig create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Podfile create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Podfile.lock create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.pbxproj create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner/AppDelegate.swift create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner/Base.lproj/Main.storyboard create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner/GoogleService-Info.plist create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner/Info.plist create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner/Runner-Bridging-Header.h create mode 100644 shelled/rustdesk-as-ref/flutter/ios/Runner/Runner.entitlements create mode 100644 shelled/rustdesk-as-ref/flutter/ios/exportOptions.plist create mode 100644 shelled/rustdesk-as-ref/flutter/ios_arm64.sh create mode 100644 shelled/rustdesk-as-ref/flutter/ios_x64.sh create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/formatter/id_formatter.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/hbbs/hbbs.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/shared_state.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/address_book.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/animated_rotation_widget.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/audio_input.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/autocomplete.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/chat_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/connection_page_title.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/custom_password.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/custom_scale_base.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/dialog.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/gestures.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/login.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/my_group.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/overlay.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/peer_card.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/peer_tab_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/peers_view.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/remote_input.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/setting_widgets.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/common/widgets/toolbar.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/consts.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/connection_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_home_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_setting_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_tab_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/file_manager_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/file_manager_tab_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/install_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/port_forward_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/port_forward_tab_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/remote_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/remote_tab_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/server_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_connection_manager.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_tab_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/view_camera_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/pages/view_camera_tab_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_port_forward_screen.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_remote_screen.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_terminal_screen.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_view_camera_screen.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/button.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/dragable_divider.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/list_search_action_listener.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/material_mod_popup_menu.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/menu_button.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/popup_menu.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/refresh_wrapper.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/remote_toolbar.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/tabbar_widget.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/titlebar_widget.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/update_progress.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/main.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/pages/connection_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/pages/file_manager_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/pages/home_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/pages/remote_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/pages/scan_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/pages/server_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/pages/settings_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/pages/terminal_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/pages/view_camera_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/widgets/custom_scale_widget.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/widgets/dialog.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/widgets/floating_mouse.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/widgets/floating_mouse_widgets.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/mobile/widgets/gesture_help.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/ab_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/chat_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/cm_file_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/desktop_render_texture.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/file_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/group_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/input_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/native_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/peer_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/peer_tab_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/platform_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/printer_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/relative_mouse_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/server_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/state_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/terminal_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/user_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/models/web_model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/native/common.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/native/custom_cursor.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/native/win32.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/plugin/common.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/plugin/event.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/plugin/handlers.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/plugin/manager.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/plugin/model.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/plugin/ui_manager.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/plugin/utils/dialogs.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/plugin/widgets/desc_ui.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/plugin/widgets/desktop_settings.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/utils/event_loop.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/utils/http_service.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/utils/image.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/utils/multi_window_manager.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/utils/platform_channel.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/utils/relative_mouse_accumulator.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/utils/scale.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/web/bridge.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/web/common.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/web/custom_cursor.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/web/dummy.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/web/plugin/handlers.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/web/settings_page.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/web/texture_rgba_renderer.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/web/web_unique.dart create mode 100644 shelled/rustdesk-as-ref/flutter/lib/web/win32.dart create mode 100644 shelled/rustdesk-as-ref/flutter/linux/.gitignore create mode 100644 shelled/rustdesk-as-ref/flutter/linux/CMakeLists.txt create mode 100644 shelled/rustdesk-as-ref/flutter/linux/bump_mouse.cc create mode 100644 shelled/rustdesk-as-ref/flutter/linux/bump_mouse.h create mode 100644 shelled/rustdesk-as-ref/flutter/linux/bump_mouse_x11.cc create mode 100644 shelled/rustdesk-as-ref/flutter/linux/bump_mouse_x11.h create mode 100644 shelled/rustdesk-as-ref/flutter/linux/flutter/CMakeLists.txt create mode 100644 shelled/rustdesk-as-ref/flutter/linux/main.cc create mode 100644 shelled/rustdesk-as-ref/flutter/linux/my_application.cc create mode 100644 shelled/rustdesk-as-ref/flutter/linux/my_application.h create mode 100644 shelled/rustdesk-as-ref/flutter/linux/wayland_shortcuts_inhibit.cc create mode 100644 shelled/rustdesk-as-ref/flutter/linux/wayland_shortcuts_inhibit.h create mode 100644 shelled/rustdesk-as-ref/flutter/macos/.gitignore create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Flutter/Flutter-Release.xcconfig create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Podfile create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Podfile.lock create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner.xcodeproj/project.pbxproj create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/AppDelegate.swift create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/AppIcon.icns create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/Configs/Debug.xcconfig create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/Configs/Release.xcconfig create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/Configs/Warnings.xcconfig create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/DebugProfile.entitlements create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/Info.plist create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/MainFlutterWindow.swift create mode 100644 shelled/rustdesk-as-ref/flutter/macos/Runner/Release.entitlements create mode 100644 shelled/rustdesk-as-ref/flutter/ndk_arm.sh create mode 100644 shelled/rustdesk-as-ref/flutter/ndk_arm64.sh create mode 100644 shelled/rustdesk-as-ref/flutter/ndk_x64.sh create mode 100644 shelled/rustdesk-as-ref/flutter/ndk_x86.sh create mode 100644 shelled/rustdesk-as-ref/flutter/pubspec.lock create mode 100644 shelled/rustdesk-as-ref/flutter/pubspec.yaml create mode 100644 shelled/rustdesk-as-ref/flutter/run.sh create mode 100644 shelled/rustdesk-as-ref/flutter/test/cm_test.dart create mode 100644 shelled/rustdesk-as-ref/flutter/windows/.gitignore create mode 100644 shelled/rustdesk-as-ref/flutter/windows/CMakeLists.txt create mode 100644 shelled/rustdesk-as-ref/flutter/windows/flutter/CMakeLists.txt create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/CMakeLists.txt create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/Runner.rc create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/flutter_window.cpp create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/flutter_window.h create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/main.cpp create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/resource.h create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/resources/app_icon.ico create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/runner.exe.manifest create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/utils.cpp create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/utils.h create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/win32_desktop.cpp create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/win32_desktop.h create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/win32_window.cpp create mode 100644 shelled/rustdesk-as-ref/flutter/windows/runner/win32_window.h create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/Cargo.toml create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/README.md create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/build.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/cliprdr.h create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/context_send.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/lib.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/filetype.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/fuse/cs.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/fuse/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/local_file.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/macos/README.md create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/macos/item_data_provider.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/macos/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/macos/paste_observer.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/macos/paste_task.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/unix/serv_files.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/platform/windows.rs create mode 100644 shelled/rustdesk-as-ref/libs/clipboard/src/windows/wf_cliprdr.c create mode 100644 shelled/rustdesk-as-ref/libs/enigo/.gitattributes create mode 100644 shelled/rustdesk-as-ref/libs/enigo/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 shelled/rustdesk-as-ref/libs/enigo/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 shelled/rustdesk-as-ref/libs/enigo/.github/ISSUE_TEMPLATE/question.md create mode 100644 shelled/rustdesk-as-ref/libs/enigo/.gitignore create mode 100644 shelled/rustdesk-as-ref/libs/enigo/.travis.yml create mode 100644 shelled/rustdesk-as-ref/libs/enigo/Cargo.toml create mode 100644 shelled/rustdesk-as-ref/libs/enigo/LICENSE create mode 100644 shelled/rustdesk-as-ref/libs/enigo/README.md create mode 100644 shelled/rustdesk-as-ref/libs/enigo/appveyor.yml create mode 100644 shelled/rustdesk-as-ref/libs/enigo/build.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/examples/dsl.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/examples/key.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/examples/keyboard.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/examples/mouse.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/examples/timer.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/rustfmt.toml create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/dsl.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/lib.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/linux/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/linux/nix_impl.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/linux/xdo.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/macos/keycodes.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/macos/macos_impl.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/macos/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/win/keycodes.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/win/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/enigo/src/win/win_impl.rs create mode 100644 shelled/rustdesk-as-ref/libs/libxdo-sys-stub/Cargo.toml create mode 100644 shelled/rustdesk-as-ref/libs/libxdo-sys-stub/src/lib.rs create mode 100644 shelled/rustdesk-as-ref/libs/portable/.gitignore create mode 100644 shelled/rustdesk-as-ref/libs/portable/Cargo.lock create mode 100644 shelled/rustdesk-as-ref/libs/portable/Cargo.toml create mode 100644 shelled/rustdesk-as-ref/libs/portable/build.rs create mode 100644 shelled/rustdesk-as-ref/libs/portable/generate.py create mode 100644 shelled/rustdesk-as-ref/libs/portable/requirements.txt create mode 100644 shelled/rustdesk-as-ref/libs/portable/src/bin_reader.rs create mode 100644 shelled/rustdesk-as-ref/libs/portable/src/main.rs create mode 100644 shelled/rustdesk-as-ref/libs/portable/src/res/spin.gif create mode 100644 shelled/rustdesk-as-ref/libs/portable/src/ui.rs create mode 100644 shelled/rustdesk-as-ref/libs/remote_printer/Cargo.toml create mode 100644 shelled/rustdesk-as-ref/libs/remote_printer/src/lib.rs create mode 100644 shelled/rustdesk-as-ref/libs/remote_printer/src/setup/driver.rs create mode 100644 shelled/rustdesk-as-ref/libs/remote_printer/src/setup/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/remote_printer/src/setup/port.rs create mode 100644 shelled/rustdesk-as-ref/libs/remote_printer/src/setup/printer.rs create mode 100644 shelled/rustdesk-as-ref/libs/remote_printer/src/setup/setup.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/.gitignore create mode 100644 shelled/rustdesk-as-ref/libs/scrap/Cargo.toml create mode 100644 shelled/rustdesk-as-ref/libs/scrap/README.md create mode 100644 shelled/rustdesk-as-ref/libs/scrap/build.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/examples/benchmark.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/examples/capture_mag.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/examples/ffplay.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/examples/list.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/examples/record-screen.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/examples/screenshot.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/android/ffi.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/android/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/bindings/aom_ffi.h create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/bindings/vpx_ffi.h create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/bindings/yuv_ffi.h create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/android.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/aom.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/camera.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/codec.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/convert.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/dxgi.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/hwcodec.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/linux.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/mediacodec.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/quartz.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/record.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/vpx.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/vpxcodec.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/vram.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/wayland.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/common/x11.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/dxgi/gdi.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/dxgi/mag.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/dxgi/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/lib.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/quartz/capturer.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/quartz/config.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/quartz/display.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/quartz/ffi.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/quartz/frame.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/quartz/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/wayland.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/wayland/README.md create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/wayland/capturable.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/wayland/display.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/wayland/pipewire.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/wayland/remote_desktop_portal.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/wayland/request_portal.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/wayland/screencast_portal.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/x11/capturer.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/x11/display.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/x11/ffi.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/x11/iter.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/x11/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/scrap/src/x11/server.rs create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/Cargo.lock create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/Cargo.toml create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/README.md create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/dylib/Cargo.toml create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/dylib/README.md create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/dylib/build.rs create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/dylib/examples/idd_controller.rs create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/dylib/src/lib.rs create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/dylib/src/win10/IddController.c create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/dylib/src/win10/IddController.h create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/dylib/src/win10/Public.h create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/dylib/src/win10/idd.rs create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/dylib/src/win10/mod.rs create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/examples/virtual_display_1.rs create mode 100644 shelled/rustdesk-as-ref/libs/virtual_display/src/lib.rs create mode 100644 shelled/rustdesk-as-ref/res/DEBIAN/postinst create mode 100644 shelled/rustdesk-as-ref/res/DEBIAN/postrm create mode 100644 shelled/rustdesk-as-ref/res/DEBIAN/preinst create mode 100644 shelled/rustdesk-as-ref/res/DEBIAN/prerm create mode 100644 shelled/rustdesk-as-ref/res/PKGBUILD create mode 100644 shelled/rustdesk-as-ref/res/ab.py create mode 100644 shelled/rustdesk-as-ref/res/audits.py create mode 100644 shelled/rustdesk-as-ref/res/bump.sh create mode 100644 shelled/rustdesk-as-ref/res/device-groups.py create mode 100644 shelled/rustdesk-as-ref/res/devices.py create mode 100644 shelled/rustdesk-as-ref/res/fdroid/patches/0000-flutter-android-x86.patch create mode 100644 shelled/rustdesk-as-ref/res/fdroid/patches/0001-x86-no-debuggable.patch create mode 100644 shelled/rustdesk-as-ref/res/gen_icon.sh create mode 100644 shelled/rustdesk-as-ref/res/icon.ico create mode 100644 shelled/rustdesk-as-ref/res/inline-sciter.py create mode 100644 shelled/rustdesk-as-ref/res/job.py create mode 100644 shelled/rustdesk-as-ref/res/lang.py create mode 100644 shelled/rustdesk-as-ref/res/manifest.xml create mode 100644 shelled/rustdesk-as-ref/res/msi/.gitignore create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/Common.h create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/CustomActions.cpp create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/CustomActions.def create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/CustomActions.vcxproj create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/DeviceUtils.cpp create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/FirewallRules.cpp create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/ReadConfig.cpp create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/RemotePrinter.cpp create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/ServiceUtils.cpp create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/dllmain.cpp create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/framework.h create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/packages.config create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/pch.cpp create mode 100644 shelled/rustdesk-as-ref/res/msi/CustomActions/pch.h create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Components/Folders.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Components/Regs.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Components/RustDesk.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Fragments/AddRemoveProperties.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Fragments/CustomActions.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Fragments/ShortcutProperties.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Fragments/Upgrades.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Includes.wxi create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Language/Package.en-us.wxl create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Language/WixExt_en-us.wxl create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/License.rtf create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Package.wixproj create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/Package.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/UI/AnotherApp.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/UI/MyInstallDirDlg.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/Package/UI/MyInstallDlg.wxs create mode 100644 shelled/rustdesk-as-ref/res/msi/README.md create mode 100644 shelled/rustdesk-as-ref/res/msi/msi.sln create mode 100644 shelled/rustdesk-as-ref/res/msi/preprocess.py create mode 100644 shelled/rustdesk-as-ref/res/osx-dist.sh create mode 100644 shelled/rustdesk-as-ref/res/pacman_install create mode 100644 shelled/rustdesk-as-ref/res/pam.d/rustdesk.debian create mode 100644 shelled/rustdesk-as-ref/res/pam.d/rustdesk.suse create mode 100644 shelled/rustdesk-as-ref/res/rpm-flutter-suse.spec create mode 100644 shelled/rustdesk-as-ref/res/rpm-flutter.spec create mode 100644 shelled/rustdesk-as-ref/res/rpm-suse.spec create mode 100644 shelled/rustdesk-as-ref/res/rpm.spec create mode 100644 shelled/rustdesk-as-ref/res/rustdesk-link.desktop create mode 100644 shelled/rustdesk-as-ref/res/rustdesk.desktop create mode 100644 shelled/rustdesk-as-ref/res/rustdesk.service create mode 100644 shelled/rustdesk-as-ref/res/startwm.sh create mode 100644 shelled/rustdesk-as-ref/res/strategies.py create mode 100644 shelled/rustdesk-as-ref/res/tray-icon.ico create mode 100644 shelled/rustdesk-as-ref/res/user-groups.py create mode 100644 shelled/rustdesk-as-ref/res/users.py create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/aom/aom-avx2.diff create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/aom/aom-install.diff create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/aom/aom-uninitialized-pointer.diff create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/aom/portfile.cmake create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/aom/vcpkg.json create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0002-fix-msvc-link.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0003-fix-windowsinclude.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0004-dependencies.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0005-fix-nasm.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0013-define-WINVER.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/0043-fix-miss-head.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/build.sh.in create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0011-android-mediacodec-encode-align-64.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/patch/0012-fix-macos-big-sur-CVBufferCopyAttachments.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/portfile.cmake create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/vcpkg-cmake-wrapper.cmake create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/ffmpeg/vcpkg.json create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libvpx/0003-add-uwp-v142-and-v143-support.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libvpx/0004-remove-library-suffixes.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libvpx/portfile.cmake create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libvpx/unofficial-libvpx-config.cmake.in create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libvpx/vcpkg.json create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libvpx/vpx.pc.in create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libyuv/fix-cmakelists.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libyuv/libyuv-config.cmake create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libyuv/portfile.cmake create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libyuv/usage create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libyuv/usage-msvc create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/libyuv/vcpkg.json create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/mfx-dispatch/fix-pkgconf.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/mfx-dispatch/portfile.cmake create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/mfx-dispatch/vcpkg.json create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/opus/fix-pkgconfig-version.patch create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/opus/portfile.cmake create mode 100644 shelled/rustdesk-as-ref/res/vcpkg/opus/vcpkg.json create mode 100644 shelled/rustdesk-as-ref/res/xorg.conf create mode 100644 shelled/rustdesk-as-ref/src/auth_2fa.rs create mode 100644 shelled/rustdesk-as-ref/src/cli.rs create mode 100644 shelled/rustdesk-as-ref/src/client.rs create mode 100644 shelled/rustdesk-as-ref/src/client/file_trait.rs create mode 100644 shelled/rustdesk-as-ref/src/client/helper.rs create mode 100644 shelled/rustdesk-as-ref/src/client/io_loop.rs create mode 100644 shelled/rustdesk-as-ref/src/client/screenshot.rs create mode 100644 shelled/rustdesk-as-ref/src/clipboard.rs create mode 100644 shelled/rustdesk-as-ref/src/clipboard_file.rs create mode 100644 shelled/rustdesk-as-ref/src/common.rs create mode 100644 shelled/rustdesk-as-ref/src/core_main.rs create mode 100644 shelled/rustdesk-as-ref/src/custom_server.rs create mode 100644 shelled/rustdesk-as-ref/src/flutter.rs create mode 100644 shelled/rustdesk-as-ref/src/flutter_ffi.rs create mode 100644 shelled/rustdesk-as-ref/src/hbbs_http.rs create mode 100644 shelled/rustdesk-as-ref/src/hbbs_http/account.rs create mode 100644 shelled/rustdesk-as-ref/src/hbbs_http/downloader.rs create mode 100644 shelled/rustdesk-as-ref/src/hbbs_http/http_client.rs create mode 100644 shelled/rustdesk-as-ref/src/hbbs_http/record_upload.rs create mode 100644 shelled/rustdesk-as-ref/src/hbbs_http/sync.rs create mode 100644 shelled/rustdesk-as-ref/src/ipc.rs create mode 100644 shelled/rustdesk-as-ref/src/kcp_stream.rs create mode 100644 shelled/rustdesk-as-ref/src/keyboard.rs create mode 100644 shelled/rustdesk-as-ref/src/lan.rs create mode 100644 shelled/rustdesk-as-ref/src/lang.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/README.md create mode 100644 shelled/rustdesk-as-ref/src/lang/ar.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/be.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/bg.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/ca.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/cn.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/cs.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/da.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/de.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/el.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/en.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/eo.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/es.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/et.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/eu.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/fa.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/fi.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/fr.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/ge.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/he.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/hr.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/hu.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/id.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/it.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/ja.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/ko.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/kz.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/lt.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/lv.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/nb.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/nl.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/pl.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/pt_PT.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/ptbr.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/ro.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/ru.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/sc.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/sk.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/sl.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/sq.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/sr.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/sv.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/ta.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/template.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/th.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/tr.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/tw.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/uk.rs create mode 100644 shelled/rustdesk-as-ref/src/lang/vi.rs create mode 100644 shelled/rustdesk-as-ref/src/lib.rs create mode 100644 shelled/rustdesk-as-ref/src/main.rs create mode 100644 shelled/rustdesk-as-ref/src/naming.rs create mode 100644 shelled/rustdesk-as-ref/src/platform/delegate.rs create mode 100644 shelled/rustdesk-as-ref/src/platform/gtk_sudo.rs create mode 100644 shelled/rustdesk-as-ref/src/platform/linux.rs create mode 100644 shelled/rustdesk-as-ref/src/platform/linux_desktop_manager.rs create mode 100644 shelled/rustdesk-as-ref/src/platform/macos.mm create mode 100644 shelled/rustdesk-as-ref/src/platform/macos.rs create mode 100644 shelled/rustdesk-as-ref/src/platform/mod.rs create mode 100644 shelled/rustdesk-as-ref/src/platform/privileges_scripts/agent.plist create mode 100644 shelled/rustdesk-as-ref/src/platform/privileges_scripts/daemon.plist create mode 100644 shelled/rustdesk-as-ref/src/platform/privileges_scripts/install.scpt create mode 100644 shelled/rustdesk-as-ref/src/platform/privileges_scripts/uninstall.scpt create mode 100644 shelled/rustdesk-as-ref/src/platform/privileges_scripts/update.scpt create mode 100644 shelled/rustdesk-as-ref/src/platform/win_device.rs create mode 100644 shelled/rustdesk-as-ref/src/platform/windows.cc create mode 100644 shelled/rustdesk-as-ref/src/platform/windows.rs create mode 100644 shelled/rustdesk-as-ref/src/platform/windows_delete_test_cert.cc create mode 100644 shelled/rustdesk-as-ref/src/plugin/callback_ext.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/callback_msg.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/config.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/desc.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/errno.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/ipc.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/manager.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/mod.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/native.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/native_handlers/macros.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/native_handlers/mod.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/native_handlers/session.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/native_handlers/ui.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/plog.rs create mode 100644 shelled/rustdesk-as-ref/src/plugin/plugins.rs create mode 100644 shelled/rustdesk-as-ref/src/port_forward.rs create mode 100644 shelled/rustdesk-as-ref/src/privacy_mode.rs create mode 100644 shelled/rustdesk-as-ref/src/privacy_mode/macos.rs create mode 100644 shelled/rustdesk-as-ref/src/privacy_mode/win_exclude_from_capture.rs create mode 100644 shelled/rustdesk-as-ref/src/privacy_mode/win_input.rs create mode 100644 shelled/rustdesk-as-ref/src/privacy_mode/win_mag.rs create mode 100644 shelled/rustdesk-as-ref/src/privacy_mode/win_topmost_window.rs create mode 100644 shelled/rustdesk-as-ref/src/privacy_mode/win_virtual_display.rs create mode 100644 shelled/rustdesk-as-ref/src/rendezvous_mediator.rs create mode 100644 shelled/rustdesk-as-ref/src/server.rs create mode 100644 shelled/rustdesk-as-ref/src/server/audio_service.rs create mode 100644 shelled/rustdesk-as-ref/src/server/clipboard_service.rs create mode 100644 shelled/rustdesk-as-ref/src/server/connection.rs create mode 100644 shelled/rustdesk-as-ref/src/server/dbus.rs create mode 100644 shelled/rustdesk-as-ref/src/server/display_service.rs create mode 100644 shelled/rustdesk-as-ref/src/server/input_service.rs create mode 100644 shelled/rustdesk-as-ref/src/server/portable_service.rs create mode 100644 shelled/rustdesk-as-ref/src/server/printer_service.rs create mode 100644 shelled/rustdesk-as-ref/src/server/rdp_input.rs create mode 100644 shelled/rustdesk-as-ref/src/server/service.rs create mode 100644 shelled/rustdesk-as-ref/src/server/terminal_helper.rs create mode 100644 shelled/rustdesk-as-ref/src/server/terminal_service.rs create mode 100644 shelled/rustdesk-as-ref/src/server/uinput.rs create mode 100644 shelled/rustdesk-as-ref/src/server/video_qos.rs create mode 100644 shelled/rustdesk-as-ref/src/server/video_service.rs create mode 100644 shelled/rustdesk-as-ref/src/server/wayland.rs create mode 100644 shelled/rustdesk-as-ref/src/service.rs create mode 100644 shelled/rustdesk-as-ref/src/tray.rs create mode 100644 shelled/rustdesk-as-ref/src/ui.rs create mode 100644 shelled/rustdesk-as-ref/src/ui/ab.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/chatbox.html create mode 100644 shelled/rustdesk-as-ref/src/ui/cm.css create mode 100644 shelled/rustdesk-as-ref/src/ui/cm.html create mode 100644 shelled/rustdesk-as-ref/src/ui/cm.rs create mode 100644 shelled/rustdesk-as-ref/src/ui/cm.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/common.css create mode 100644 shelled/rustdesk-as-ref/src/ui/common.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/file_transfer.css create mode 100644 shelled/rustdesk-as-ref/src/ui/file_transfer.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/grid.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/header.css create mode 100644 shelled/rustdesk-as-ref/src/ui/header.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/index.css create mode 100644 shelled/rustdesk-as-ref/src/ui/index.html create mode 100644 shelled/rustdesk-as-ref/src/ui/index.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/install.html create mode 100644 shelled/rustdesk-as-ref/src/ui/install.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/msgbox.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/port_forward.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/printer.tis create mode 100644 shelled/rustdesk-as-ref/src/ui/remote.css create mode 100644 shelled/rustdesk-as-ref/src/ui/remote.html create mode 100644 shelled/rustdesk-as-ref/src/ui/remote.rs create mode 100644 shelled/rustdesk-as-ref/src/ui/remote.tis create mode 100644 shelled/rustdesk-as-ref/src/ui_cm_interface.rs create mode 100644 shelled/rustdesk-as-ref/src/ui_interface.rs create mode 100644 shelled/rustdesk-as-ref/src/ui_session_interface.rs create mode 100644 shelled/rustdesk-as-ref/src/updater.rs create mode 100644 shelled/rustdesk-as-ref/src/virtual_display_manager.rs create mode 100644 shelled/rustdesk-as-ref/src/whiteboard/client.rs create mode 100644 shelled/rustdesk-as-ref/src/whiteboard/linux.rs create mode 100644 shelled/rustdesk-as-ref/src/whiteboard/macos.rs create mode 100644 shelled/rustdesk-as-ref/src/whiteboard/mod.rs create mode 100644 shelled/rustdesk-as-ref/src/whiteboard/server.rs create mode 100644 shelled/rustdesk-as-ref/src/whiteboard/win_linux.rs create mode 100644 shelled/rustdesk-as-ref/src/whiteboard/windows.rs create mode 100644 shelled/rustdesk-as-ref/vcpkg.json create mode 100644 shelled/shelled-os-ui/src-tauri/Cargo.lock create mode 100644 shelled/shelled-os-ui/src-tauri/gen/schemas/acl-manifests.json create mode 100644 shelled/shelled-os-ui/src-tauri/gen/schemas/capabilities.json create mode 100644 shelled/shelled-os-ui/src-tauri/gen/schemas/desktop-schema.json create mode 100644 shelled/shelled-os-ui/src-tauri/gen/schemas/windows-schema.json diff --git a/shelled/plan.md b/shelled/plan-os-ui.md similarity index 100% rename from shelled/plan.md rename to shelled/plan-os-ui.md diff --git a/shelled/rustdesk-as-ref/.cargo/config.toml b/shelled/rustdesk-as-ref/.cargo/config.toml new file mode 100644 index 0000000..42a4adb --- /dev/null +++ b/shelled/rustdesk-as-ref/.cargo/config.toml @@ -0,0 +1,16 @@ +[target.x86_64-pc-windows-msvc] +rustflags = ["-Ctarget-feature=+crt-static"] +[target.i686-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"] +[target.'cfg(target_os="macos")'] +rustflags = [ + "-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null", +] +#[target.'cfg(target_os="linux")'] +# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change +# this is unlikely to help also, because the other so files still use libc dynamically +#rustflags = [ +# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic" +#] +[net] +git-fetch-with-cli = true diff --git a/shelled/rustdesk-as-ref/.gitattributes b/shelled/rustdesk-as-ref/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/shelled/rustdesk-as-ref/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/shelled/rustdesk-as-ref/.github/FUNDING.yml b/shelled/rustdesk-as-ref/.github/FUNDING.yml new file mode 100644 index 0000000..1745f9b --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [rustdesk] +ko_fi: rustdesk diff --git a/shelled/rustdesk-as-ref/.github/ISSUE_TEMPLATE/bug_report.yaml b/shelled/rustdesk-as-ref/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..dbb8ed8 --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,55 @@ +name: 🞠Bug report +description: Thanks for taking the time to fill out this bug report! Please fill the form in **English** +labels: ["bug"] +body: + - type: textarea + id: desc + attributes: + label: Bug Description + description: A clear and concise description of what the bug is (if it's a keyboard issue, provide the keyboard mode you're using. e.g. legacy, map, translate) + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: How to Reproduce + description: What steps can we take to reproduce this behavior? + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen + validations: + required: true + - type: input + id: os + attributes: + label: Operating system(s) on local (controlling) side and remote (controlled) side + description: What operating system(s) do you see this bug on? local (controlling) side -> remote (controlled) side. + placeholder: | + Windows 10 -> osx + validations: + required: true + - type: input + id: version + attributes: + label: RustDesk Version(s) on local (controlling) side and remote (controlled) side + description: What RustDesk version(s) do you see this bug on? local (controlling) side -> remote (controlled) side. + placeholder: | + 1.1.9 -> 1.1.8 + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Please add screenshots to help explain your problem, if applicable, please upload video. + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any additonal context about the problem here diff --git a/shelled/rustdesk-as-ref/.github/ISSUE_TEMPLATE/config.yml b/shelled/rustdesk-as-ref/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8a5429b --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Feature Request + url: https://github.com/rustdesk/rustdesk/discussions/categories/feature-request + about: Discuss ideas for new features or enhancements, it will be converted to GitHub issue when we commit to building those changes or are helping a community member contribute their own changes + - name: Ask a question + url: https://github.com/rustdesk/rustdesk/discussions/category_choices + about: Ask questions and discuss with other community members. diff --git a/shelled/rustdesk-as-ref/.github/dependabot.yml b/shelled/rustdesk-as-ref/.github/dependabot.yml new file mode 100644 index 0000000..56258e4 --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "gitsubmodule" + directory: "/" + target-branch: "master" + schedule: + interval: "daily" + commit-message: + prefix: "Git submodule" + labels: + - "dependencies" diff --git a/shelled/rustdesk-as-ref/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff b/shelled/rustdesk-as-ref/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff new file mode 100644 index 0000000..9b8ea26 --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff @@ -0,0 +1,42 @@ +diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart +index 7e634cd2aa..c1e9acc295 100644 +--- a/packages/flutter/lib/src/material/dropdown_menu.dart ++++ b/packages/flutter/lib/src/material/dropdown_menu.dart +@@ -475,7 +475,7 @@ class _DropdownMenuState extends State> { + final GlobalKey _leadingKey = GlobalKey(); + late List buttonItemKeys; + final MenuController _controller = MenuController(); +- late bool _enableFilter; ++ bool _enableFilter = false; + late List> filteredEntries; + List? _initialMenu; + int? currentHighlight; +@@ -524,6 +524,11 @@ class _DropdownMenuState extends State> { + } + _localTextEditingController = widget.controller ?? TextEditingController(); + } ++ if (oldWidget.enableFilter != widget.enableFilter) { ++ if (!widget.enableFilter) { ++ _enableFilter = false; ++ } ++ } + if (oldWidget.enableSearch != widget.enableSearch) { + if (!widget.enableSearch) { + currentHighlight = null; +@@ -663,6 +668,7 @@ class _DropdownMenuState extends State> { + ); + currentHighlight = widget.enableSearch ? i : null; + widget.onSelected?.call(entry.value); ++ _enableFilter = false; + } + : null, + requestFocusOnHover: false, +@@ -735,6 +741,8 @@ class _DropdownMenuState extends State> { + if (_enableFilter) { + filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text) + ?? filter(widget.dropdownMenuEntries, _localTextEditingController!); ++ } else { ++ filteredEntries = widget.dropdownMenuEntries; + } + + if (widget.enableSearch) { diff --git a/shelled/rustdesk-as-ref/.github/workflows/bridge.yml b/shelled/rustdesk-as-ref/.github/workflows/bridge.yml new file mode 100644 index 0000000..1913132 --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/workflows/bridge.yml @@ -0,0 +1,98 @@ +# This yaml shares the build bridge steps with ci and nightly. +name: Build flutter-rust-bridge +# 2023-11-23 18:00:00+00:00 + +on: + workflow_call: + +env: + CARGO_EXPAND_VERSION: "1.0.95" + FLUTTER_VERSION: "3.22.3" + FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 + +jobs: + generate_bridge: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-22.04, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install prerequisites + run: | + sudo apt-get install ca-certificates -y + sudo apt-get update -y + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + libclang-dev \ + libgtk-3-dev \ + llvm-dev \ + nasm \ + ninja-build \ + pkg-config \ + wget + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: bridge-${{ matrix.job.os }} + + - name: Cache Bridge + id: cache-bridge + uses: actions/cache@v3 + with: + path: /tmp/flutter_rust_bridge + key: vcpkg-${{ matrix.job.arch }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked + pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd + + - name: Run flutter rust bridge + run: | + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h + cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h + + - name: Upload Artifact + uses: actions/upload-artifact@master + with: + name: bridge-artifact + path: | + ./src/bridge_generated.rs + ./src/bridge_generated.io.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart + ./flutter/macos/Runner/bridge_generated.h + ./flutter/ios/Runner/bridge_generated.h diff --git a/shelled/rustdesk-as-ref/.github/workflows/ci.yml b/shelled/rustdesk-as-ref/.github/workflows/ci.yml new file mode 100644 index 0000000..3a7d21d --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/workflows/ci.yml @@ -0,0 +1,250 @@ +name: CI + +env: +# MIN_SUPPORTED_RUST_VERSION: "1.46.0" +# CICD_INTERMEDIATES_DIR: "_cicd-intermediates" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" + +on: + workflow_dispatch: + pull_request: + paths-ignore: + - "docs/**" + - "README.md" + push: + branches: + - master + paths-ignore: + - ".github/**" + - "docs/**" + - "README.md" + - "res/**" + - "appimage/**" + - "flatpak/**" + +jobs: + # ensure_cargo_fmt: + # name: Ensure 'cargo fmt' has been run + # runs-on: ubuntu-20.04 + # steps: + # - uses: actions-rs/toolchain@v1 + # with: + # toolchain: stable + # default: true + # profile: minimal + # components: rustfmt + # - uses: actions/checkout@v3 + # - run: cargo fmt -- --check + + # min_version: + # name: Minimum supported rust version + # runs-on: ubuntu-20.04 + # steps: + # - name: Checkout source code + # uses: actions/checkout@v3 + # with: + # submodules: recursive + + # - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }}) + # uses: actions-rs/toolchain@v1 + # with: + # toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }} + # default: true + # profile: minimal # minimal component installation (ie, no documentation) + # components: clippy + # - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix) + # uses: actions-rs/cargo@v1 + # with: + # command: clippy + # args: --locked --all-targets --all-features -- --allow clippy::unknown_clippy_lints + # - name: Run tests + # uses: actions-rs/cargo@v1 + # with: + # command: test + # args: --locked + + build: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + # - { target: x86_64-apple-darwin , os: macos-10.15 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + # - { target: x86_64-pc-windows-msvc , os: windows-2022 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Free Disk Space (Ubuntu) + if: runner.os == 'Linux' + # jlumbroso/free-disk-space@main is used in .github\workflows\flutter-build.yml + # But pinning to a specific version to avoid unexpected issues is preferred. + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false + + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install prerequisites + shell: bash + run: | + case ${{ matrix.job.target }} in + x86_64-unknown-linux-gnu) + sudo apt-get -y update + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + libpam0g-dev \ + libasound2-dev \ + libunwind-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpulse-dev \ + libva-dev \ + libvdpau-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + nasm \ + wget + ;; + # 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: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed" + shell: bash + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + targets: ${{ matrix.job.target }} + components: '' + + - name: Show version information (Rust, cargo, GCC) + shell: bash + run: | + gcc --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - uses: Swatinem/rust-cache@v2 + + - name: Build + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.job.use-cross }} + command: build + args: --locked --target=${{ matrix.job.target }} + + - name: clean + shell: bash + run: | + cargo clean + + # - name: Strip debug information from executable + # id: strip + # shell: bash + # run: | + # # Figure out suffix of binary + # EXE_suffix="" + # case ${{ matrix.job.target }} in + # *-pc-windows-*) EXE_suffix=".exe" ;; + # esac; + + # # Figure out what strip tool to use if any + # STRIP="strip" + # case ${{ matrix.job.target }} in + # arm-unknown-linux-*) STRIP="arm-linux-gnueabihf-strip" ;; + # aarch64-unknown-linux-gnu) STRIP="aarch64-linux-gnu-strip" ;; + # *-pc-windows-msvc) STRIP="" ;; + # esac; + + # # Setup paths + # BIN_DIR="${{ env.CICD_INTERMEDIATES_DIR }}/stripped-release-bin/" + # mkdir -p "${BIN_DIR}" + # BIN_NAME="${{ env.PROJECT_NAME }}${EXE_suffix}" + # BIN_PATH="${BIN_DIR}/${BIN_NAME}" + + # # Copy the release build binary to the result location + # cp "target/${{ matrix.job.target }}/release/${BIN_NAME}" "${BIN_DIR}" + + # # Also strip if possible + # if [ -n "${STRIP}" ]; then + # "${STRIP}" "${BIN_PATH}" + # fi + + # # Let subsequent steps know where to find the (stripped) bin + # echo ::set-output name=BIN_PATH::${BIN_PATH} + # echo ::set-output name=BIN_NAME::${BIN_NAME} + + - name: Set testing options + id: test-options + shell: bash + run: | + # test only library unit tests and binary for arm-type targets + unset CARGO_TEST_OPTIONS + + case ${{ matrix.job.target }} in + arm-* | aarch64-*) + CARGO_TEST_OPTIONS="--lib --bin ${PROJECT_NAME}" + ;; + *) + CARGO_TEST_OPTIONS="--workspace --no-fail-fast -- --skip test_get_cursor_pos --skip test_get_key_state" + ;; + esac; + + #deprecated echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} + echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_ENV + echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT + + - name: Run tests + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.job.use-cross }} + command: test + args: --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}} diff --git a/shelled/rustdesk-as-ref/.github/workflows/clear-cache.yml b/shelled/rustdesk-as-ref/.github/workflows/clear-cache.yml new file mode 100644 index 0000000..cd94cab --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/workflows/clear-cache.yml @@ -0,0 +1,37 @@ +name: Clear cache + +on: + workflow_dispatch: + +permissions: + actions: write + +jobs: + clear-cache: + runs-on: ubuntu-latest + steps: + - name: Clear cache + uses: actions/github-script@v7 + with: + script: | + console.log("About to clear") + const caches = await github.rest.actions.getActionsCacheList({ + owner: context.repo.owner, + repo: context.repo.repo, + }) + for (const cache of caches.data.actions_caches) { + console.log(cache) + github.rest.actions.deleteActionsCacheById({ + owner: context.repo.owner, + repo: context.repo.repo, + cache_id: cache.id, + }) + } + console.log("Clear completed") + + - name: Purge cache # Above seems not clear thouroughly, so add this to double clear + uses: MyAlbum/purge-cache@v2 + with: + accessed: true # Purge caches by their last accessed time (default) + created: false # Purge caches by their created time (default) + max-age: 1 # in seconds diff --git a/shelled/rustdesk-as-ref/.github/workflows/fdroid.yml b/shelled/rustdesk-as-ref/.github/workflows/fdroid.yml new file mode 100644 index 0000000..94f8d3d --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/workflows/fdroid.yml @@ -0,0 +1,39 @@ +name: Fdroid version file generation + +on: + workflow_dispatch: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+-[0-9]+' + +jobs: + # https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.carriez.flutter_hbb.yml + # Finds latest release and transforms F-Droid version code from version as follows: + # X.Y.Z-A => X * 1e6 + Y * 1e4 + Z * 1e2 + A + update-fdroid-version-file: + name: Publish RustDesk version file for F-Droid updater + runs-on: ubuntu-latest + steps: + - name: Generate RustDesk version file + run: | + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + UPSTREAM_VERNAME="${GITHUB_REF##refs/tags/}" + UPSTREAM_VERNAME="${UPSTREAM_VERNAME##v}" + else + UPSTREAM_VERNAME="$(curl https://api.github.com/repos/rustdesk/rustdesk/releases/latest | jq -r .tag_name | sed 's/^v//')" + fi + UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr '.' ' ' | tr '-' ' ' | while read -r MAJOR MINOR PATCH REV; do [ -z "$MAJOR" ] && MAJOR=0; [ -z "$MINOR" ] && MINOR=0; [ -z "$PATCH" ] && PATCH=0; [ -z "$REV" ] && REV=0; echo "$(( 1000000 * $MAJOR + 10000 * $MINOR + 100 * $PATCH + $REV ))"; done)" + echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt + echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt + shell: bash + + - name: Publish RustDesk version file + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: "fdroid-version" + files: | + ./rustdesk-version.txt diff --git a/shelled/rustdesk-as-ref/.github/workflows/flutter-build.yml b/shelled/rustdesk-as-ref/.github/workflows/flutter-build.yml new file mode 100644 index 0000000..263bd67 --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/workflows/flutter-build.yml @@ -0,0 +1,2062 @@ +name: Build the flutter version of the RustDesk + +on: + workflow_call: + inputs: + upload-artifact: + type: boolean + default: true + upload-tag: + type: string + default: "nightly" + +# NOTE: F-Droid builder script 'flutter/build_fdroid.sh' reads environment +# variables from this workflow! +# +# It does NOT read build steps, however, so please fix 'flutter/build_fdroid.sh +# whenever you add changes to Android CI build action ('build-rustdesk-android') +# in this file! + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81 + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.15.05-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # vcpkg version: 2025.08.27 + # If we change the `VCPKG COMMIT_ID`, please remember: + # 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`. + # Or we may face build issue like + # https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174 + # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. + VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version + VERSION: "1.4.6" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}" + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}-2" + +jobs: + generate-bridge: + uses: ./.github/workflows/bridge.yml + + build-RustDeskTempTopMostWindow: + uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml + with: + upload-artifact: ${{ inputs.upload-artifact }} + target: windows-2022 + configuration: Release + platform: x64 + target_version: Windows10 + strategy: + fail-fast: false + + build-for-windows-flutter: + name: ${{ matrix.job.target }} + needs: [build-RustDeskTempTopMostWindow, generate-bridge] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + - { + target: x86_64-pc-windows-msvc, + os: windows-2022, + arch: x86_64, + vcpkg-triplet: x64-windows-static, + } + # - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 } + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + # https://github.com/flutter/flutter/issues/155685 + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip + Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release + mv -Force windows-x64-release/*  C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Patch flutter + shell: bash + run: | + cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter))) + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.SCITER_RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: C:\vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + env: + VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} + run: | + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + shell: bash + + - name: Build rustdesk + run: | + # Windows: build RustDesk + python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack + mv ./flutter/build/windows/x64/runner/Release ./rustdesk + + # Download usbmmidd_v2.zip and extract it to ./rustdesk + Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip + Expand-Archive usbmmidd_v2.zip -DestinationPath . + Remove-Item -Path usbmmidd_v2\Win32 -Recurse + Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat" + mv -Force .\usbmmidd_v2 ./rustdesk + + # Download printer driver files and extract them to ./rustdesk + try { + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums + + # Check and move the files + $checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4-1.4\.zip$').Matches.Groups[1].Value + $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4-1.4.zip -Algorithm SHA256 + $checksum_adapter = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value + $downloadsum_adapter = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256 + if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_adapter -eq $downloadsum_adapter.Hash) { + Write-Output "rustdesk_printer_driver_v4-1.4, checksums match, extract the file." + Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath . + mkdir ./rustdesk/drivers + mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver + Expand-Archive printer_driver_adapter.zip -DestinationPath . + mv -Force .\printer_driver_adapter.dll ./rustdesk + } elseif ($checksum_driver -ne $downloadsum_driver.Hash) { + Write-Output "rustdesk_printer_driver_v4-1.4, checksums do not match, ignore the file." + } else { + Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file." + } + } catch { + Write-Host "Ingore the printer driver error." + } + + - name: find Runner.res + # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res + # Runner.rc does not contain actual version, but Runner.res does + continue-on-error: true + shell: bash + run: | + runner_res=$(find . -name "Runner.res"); + if [ "$runner_res" == "" ]; then + echo "Runner.res: not found"; + else + echo "Runner.res: $runner_res"; + cp $runner_res ./libs/portable/Runner.res; + echo "list ./libs/portable/Runner.res"; + ls -l ./libs/portable/Runner.res; + fi + + - name: Download RustDeskTempTopMostWindow artifacts + uses: actions/download-artifact@master + if: ${{ inputs.upload-artifact }} + with: + name: topmostwindow-artifacts + path: "./rustdesk" + + - name: Upload unsigned + if: env.UPLOAD_ARTIFACT == 'true' + uses: actions/upload-artifact@master + with: + name: rustdesk-unsigned-windows-${{ matrix.job.arch }} + path: rustdesk + + - name: Sign rustdesk files + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + shell: bash + run: | + pip3 install requests argparse + BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/ + + - name: Build self-extracted executable + shell: bash + if: env.UPLOAD_ARTIFACT == 'true' + run: | + sed -i '/dpiAware/d' res/manifest.xml + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../rustdesk/ -o . -e ../../rustdesk/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.exe + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Build msi + if: env.UPLOAD_ARTIFACT == 'true' + run: | + pushd ./res/msi + python preprocess.py --arp -d ../../rustdesk + nuget restore msi.sln + msbuild msi.sln -p:Configuration=Release -p:Platform=x64 /p:TargetVersion=Windows10 + mv ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.msi + sha256sum ../../SignOutput/rustdesk-*.msi + + - name: Sign rustdesk self-extracted file + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + shell: bash + run: | + BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput + + - name: Publish Release + uses: softprops/action-gh-release@v1 + if: env.UPLOAD_ARTIFACT == 'true' + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.msi + ./SignOutput/rustdesk-*.exe + + # The fallback for the flutter version, we use Sciter for 32bit Windows. + build-for-windows-sciter: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + # Temporarily disable this action due to additional test is needed. + # if: false + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + - { + target: i686-pc-windows-msvc, + os: windows-2022, + arch: x86, + vcpkg-triplet: x86-windows-static, + } + # - { target: aarch64-pc-windows-msvc, os: windows-2022 } + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install LLVM and Clang + uses: rustdesk-org/install-llvm-action-32bit@master + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: nightly-2023-10-13-${{ matrix.job.target }} # must use nightly here, because of abi_thiscall feature required + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }}-sciter + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: C:\vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + env: + VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} + run: | + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + shell: bash + + - name: Build rustdesk + id: build + shell: bash + run: | + python3 res/inline-sciter.py + # Patch sciter x86 + sed -i 's/branch = "dyn"/branch = "dyn_x86"/g' ./Cargo.toml + cargo build --features inline,vram,hwcodec --release --bins + mkdir -p ./Release + mv ./target/release/rustdesk.exe ./Release/rustdesk.exe + curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll + echo "output_folder=./Release" >> $GITHUB_OUTPUT + curl -LJ -o ./usbmmidd_v2.zip https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip + unzip usbmmidd_v2.zip + # Do not remove x64 files, because the user may run the 32bit version on a 64bit system. + # Do not remove ./usbmmidd_v2/deviceinstaller64.exe, as x86 exe cannot install and uninstall drivers when running on x64, + # we need the x64 exe to install and uninstall the driver. + rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat + mv ./usbmmidd_v2 ./Release || true + + - name: find Runner.res + # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res + # Runner.rc does not contain actual version, but Runner.res does + continue-on-error: true + shell: bash + run: | + runner_res=$(find . -name "Runner.res"); + if [ "$runner_res" == "" ]; then + echo "Runner.res: not found"; + else + echo "Runner.res: $runner_res"; + cp $runner_res ./libs/portable/Runner.res; + echo "list ./libs/portable/Runner.res"; + ls -l ./libs/portable/Runner.res; + fi + + - name: Upload unsigned + if: env.UPLOAD_ARTIFACT == 'true' + uses: actions/upload-artifact@master + with: + name: rustdesk-unsigned-windows-${{ matrix.job.arch }} + path: Release + + - name: Sign rustdesk files + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + shell: bash + run: | + pip3 install requests argparse + BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/ + + - name: Build self-extracted executable + shell: bash + run: | + sed -i '/dpiAware/d' res/manifest.xml + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../Release/ -o . -e ../../Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe + + - name: Sign rustdesk self-extracted file + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' + shell: bash + run: | + BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/ + + - name: Publish Release + uses: softprops/action-gh-release@v1 + if: env.UPLOAD_ARTIFACT == 'true' + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.exe + + build-rustdesk-ios: + if: ${{ inputs.upload-artifact }} + name: build rustdesk ios ipa + runs-on: ${{ matrix.job.os }} + needs: [generate-bridge] + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-apple-ios, + os: macos-latest, + vcpkg-triplet: arm64-ios, + } + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Install dependencies + run: | + brew install nasm yasm + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + run: | + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + shell: bash + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache-ios + key: ${{ matrix.job.target }} + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Build rustdesk lib + run: | + rustup target add ${{ matrix.job.target }} + cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib + + - name: Upload liblibrustdesk.a Artifacts + uses: actions/upload-artifact@master + with: + name: liblibrustdesk.a + path: target/aarch64-apple-ios/release/liblibrustdesk.a + + - name: Build rustdesk + shell: bash + run: | + pushd flutter + # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign + # for easy debugging + flutter build ipa --release --no-codesign + + # - name: Upload Artifacts + # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + # uses: actions/upload-artifact@master + # with: + # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + # path: flutter/build/ios/ipa/*.ipa + + # - name: Publish ipa package + # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # flutter/build/ios/ipa/*.ipa + + + build-for-macOS: + name: ${{ matrix.job.target }} + runs-on: ${{ matrix.job.os }} + needs: [generate-bridge] + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-15-intel, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel + extra-build-args: "", + arch: x86_64, + vcpkg-triplet: x64-osx, + } + - { + target: aarch64-apple-darwin, + os: macos-14, + # extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296 + extra-build-args: "--screencapturekit", + arch: aarch64, + vcpkg-triplet: arm64-osx, + } + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Import the codesign cert + if: env.MACOS_P12_BASE64 != null + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + keychain: rustdesk + + - name: Check sign and import sign key + if: env.MACOS_P12_BASE64 != null + run: | + security default-keychain -s rustdesk.keychain + security find-identity -v + + - name: Import notarize key + if: env.MACOS_P12_BASE64 != null + uses: timheuer/base64-to-file@v1.2 + with: + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling + fileName: rustdesk.json + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} + + - name: Install rcodesign tool + if: env.MACOS_P12_BASE64 != null + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + + - name: Install build runtime + run: | + brew install llvm create-dmg + # pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner + if command -v pkg-config &>/dev/null; then + echo "pkg-config is already installed" + else + brew install pkg-config + fi + + - name: Install NASM + run: | + # Install NASM 2.16.x from official release. + # Do NOT use `brew install nasm` which installs NASM 3.x. + # NASM 3.x is a complete rewrite with incompatible CLI options and removed features. + # aom and other multimedia libraries require NASM 2.x for x86/x86_64 assembly. + wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/macosx/nasm-2.16.03-macosx.zip + unzip nasm-2.16.03-macosx.zip + sudo cp nasm-2.16.03/nasm /usr/local/bin/nasm + nasm --version + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + + - name: Workaround for flutter issue + shell: bash + run: | + cd "$(dirname "$(which flutter)")" + # https://github.com/flutter/flutter/issues/133533 + sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart + grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.MAC_RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + run: | + if ! $VCPKG_ROOT/vcpkg \ + install \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + + - name: Show version information (Rust, cargo, Clang) + shell: bash + run: | + clang --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build rustdesk + run: | + if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then + MIN_MACOS_VERSION="12.3" + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py + sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile + sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj + fi + ./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }} + + - name: create unsigned dmg + if: env.UPLOAD_ARTIFACT == 'true' + run: | + CREATE_DMG="$(command -v create-dmg)" + CREATE_DMG="$(readlink -f "$CREATE_DMG")" + sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG" + create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app + + - name: Upload unsigned macOS app + if: env.UPLOAD_ARTIFACT == 'true' + uses: actions/upload-artifact@master + with: + name: rustdesk-unsigned-macos-${{ matrix.job.arch }} + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed + + - name: Codesign app and create signed dmg + if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true' + run: | + # Patch create-dmg to give more attempts to unmount image + CREATE_DMG="$(command -v create-dmg)" + CREATE_DMG="$(readlink -f "$CREATE_DMG")" + sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG" + # Unlock keychain + security default-keychain -s rustdesk.keychain + security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain + # start sign the rustdesk.app and dmg + rm -rf *.dmg || true + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv + create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv + # notarize the rustdesk-${{ env.VERSION }}.dmg + rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg + + - name: Rename rustdesk + if: env.UPLOAD_ARTIFACT == 'true' + run: | + for name in rustdesk*??.dmg; do + mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}.dmg" + done + + - name: Publish DMG package + if: env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk*-${{ matrix.job.arch }}.dmg + + publish_unsigned: + needs: + - build-for-macOS + - build-for-windows-flutter + - build-for-windows-sciter + runs-on: ubuntu-latest + if: ${{ inputs.upload-artifact }} + steps: + - name: Download artifacts + uses: actions/download-artifact@master + with: + name: rustdesk-unsigned-macos-x86_64 + path: ./ + + - name: Download Artifacts + uses: actions/download-artifact@master + with: + name: rustdesk-unsigned-macos-aarch64 + path: ./ + + - name: Download Artifacts + uses: actions/download-artifact@master + with: + name: rustdesk-unsigned-windows-x86_64 + path: ./windows-x86_64/ + + - name: Download Artifacts + uses: actions/download-artifact@master + with: + name: rustdesk-unsigned-windows-x86 + path: ./windows-x86/ + + - name: Combine unsigned app + run: | + tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-x86 + + - name: Publish unsigned app + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz + + build-rustdesk-android: + needs: [generate-bridge] + name: build rustdesk android apk ${{ matrix.job.target }} + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-linux-android, + os: ubuntu-24.04, + reltype: release, + suffix: "", + } + - { + arch: armv7, + target: armv7-linux-androideabi, + os: ubuntu-24.04, + reltype: release, + suffix: "", + } + - { + arch: x86_64, + target: x86_64-linux-android, + os: ubuntu-24.04, + reltype: release, + suffix: "", + } + steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false + + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc-multilib \ + git \ + g++ \ + g++-multilib \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libc6-dev \ + libclang-dev \ + libunwind-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-dev \ + nasm \ + ninja-build \ + openjdk-17-jdk-headless \ + pkg-config \ + tree \ + wget + + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} + + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: ${{ env.NDK_VERSION }} + add-to-path: true + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + run: | + case ${{ matrix.job.target }} in + aarch64-linux-android) + ANDROID_TARGET=arm64-v8a + ;; + armv7-linux-androideabi) + ANDROID_TARGET=armeabi-v7a + ;; + x86_64-linux-android) + ANDROID_TARGET=x86_64 + ;; + i686-linux-android) + ANDROID_TARGET=x86 + ;; + esac + if ! ./flutter/build_android_deps.sh "${ANDROID_TARGET}"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + shell: bash + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated + key: ${{ matrix.job.target }} + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + x86_64-linux-android) + ./flutter/ndk_x64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64 + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + ;; + i686-linux-android) + ./flutter/ndk_x86.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/x86 + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + ;; + esac + + - name: Upload Rustdesk library to Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk.so.${{ matrix.job.target }} + path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH + # Increase Gradle JVM memory for CI builds + sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + x86_64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64 + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x64 --split-per-abi + mv build/app/outputs/flutter-apk/app-x86_64-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + i686-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/x86 + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x86 --split-per-abi + mv build/app/outputs/flutter-apk/app-x86-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk . + + # https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075 + - name: Setup sign tool version variable + shell: bash + run: | + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo Last build tool version is: $BUILD_TOOL_VERSION + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + if: env.ANDROID_SIGNING_KEY != null + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + env: + # env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable" + BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }} + + - name: Upload Artifacts + if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + uses: actions/upload-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish signed apk package + if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish unsigned apk package + if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + + build-rustdesk-android-universal: + needs: [build-rustdesk-android] + name: build rustdesk android universal apk + if: ${{ inputs.upload-artifact }} + runs-on: ubuntu-24.04 + env: + reltype: release + x86_target: "" # can be ",android-x86" + suffix: "" + steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false + + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc-multilib \ + git \ + g++ \ + g++-multilib \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libc6-dev \ + libclang-dev \ + libunwind-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-dev \ + nasm \ + ninja-build \ + openjdk-17-jdk-headless \ + pkg-config \ + tree \ + wget + + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} + + - name: Patch flutter + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Download Rustdesk library from Artifacts + uses: actions/download-artifact@master + with: + name: librustdesk.so.aarch64-linux-android + path: ./flutter/android/app/src/main/jniLibs/arm64-v8a + + - name: Download Rustdesk library from Artifacts + uses: actions/download-artifact@master + with: + name: librustdesk.so.armv7-linux-androideabi + path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a + + - name: Download Rustdesk library from Artifacts + uses: actions/download-artifact@master + with: + name: librustdesk.so.x86_64-linux-android + path: ./flutter/android/app/src/main/jniLibs/x86_64 + + - name: Download Rustdesk library from Artifacts + if: ${{ env.reltype == 'debug' }} + uses: actions/download-artifact@master + with: + name: librustdesk.so.i686-linux-android + path: ./flutter/android/app/src/main/jniLibs/x86 + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH + # Increase Gradle JVM memory for CI builds + sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ + mv ./flutter/android/app/src/main/jniLibs/armeabi-v7a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ + mv ./flutter/android/app/src/main/jniLibs/x86_64/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/ + if [ "${{ env.reltype }}" = "debug" ]; then + mv ./flutter/android/app/src/main/jniLibs/x86/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/ + fi + # build flutter + pushd flutter + flutter build apk "--${{ env.reltype }}" --target-platform android-arm64,android-arm,android-x64${{ env.x86_target }} + popd + mkdir -p signed-apk + mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk + + # https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075 + - name: Setup sign tool version variable + shell: bash + run: | + BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1) + echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV + echo Last build tool version is: $BUILD_TOOL_VERSION + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + if: env.ANDROID_SIGNING_KEY != null + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + env: + # env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable" + BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }} + + - name: Upload Artifacts + if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + uses: actions/upload-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish signed apk package + if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish unsigned apk package + if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk + + build-rustdesk-linux: + needs: [generate-bridge] + name: build rustdesk linux ${{ matrix.job.target }} + runs-on: ${{ matrix.job.on }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + distro: ubuntu18.04, + on: ubuntu-22.04, + deb_arch: amd64, + vcpkg-triplet: x64-linux, + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + distro: ubuntu18.04, + on: ubuntu-22.04-arm, + deb_arch: arm64, + vcpkg-triplet: arm64-linux, + } + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt-get update -y + sudo apt-get install -y nasm + if [[ "${{ matrix.job.arch }}" == "x86_64" ]]; then + sudo apt-get install -y qemu-user-static + fi + + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set Swap Space + if: ${{ matrix.job.arch == 'x86_64' }} + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df -h + free -m + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + with: + toolchain: ${{ env.RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - name: Save Rust toolchain version + run: | + RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}') + echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV + + - name: Disable rust bridge build + run: | + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Setup vcpkg with Github Actions binary cache + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + run: | + sudo apt install -y libva-dev && apt show libva-dev + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + shell: bash + + - name: Restore bridge files + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - uses: rustdesk-org/run-on-arch-action@amd64-support + name: Build rustdesk + id: vcpkg + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + with: + arch: ${{ matrix.job.arch }} + distro: ${{ matrix.job.distro }} + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt-get update -y + echo -e "installing deps" + apt-get install -y \ + build-essential \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libclang-10-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-10-dev \ + nasm \ + ninja-build \ + pkg-config \ + tree \ + python3 \ + rpm \ + unzip \ + wget \ + xz-utils \ + libssl-dev + # we have libopus compiled by us. + apt-get remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + # do not use rustup, because memory overflow in qemu + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh + rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + export VCPKG_ROOT=/opt/artifacts/vcpkg + if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then + export JOBS="--jobs 3" + else + export JOBS="" + fi + echo $JOBS + cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release + rm -rf target/release/deps target/release/build + rm -rf ~/.cargo + + # Setup Flutter + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + case ${{ matrix.job.arch }} in + aarch64) + export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux --verbose/g" ./build.py + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + x86_64) + export PATH=/opt/flutter/bin:$PATH + ;; + esac + popd + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + case ${{ matrix.job.arch }} in + aarch64) + # clone repo and reset to flutter ${{ env.FLUTTER_VERSION }} + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + git fetch + git reset --hard ${{ env.FLUTTER_VERSION }} + bin/flutter-elinux doctor -v + bin/flutter-elinux precache --linux + popd + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + rm -rf flutter + ;; + x86_64) + flutter doctor -v + ;; + esac + + if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then + case ${{ matrix.job.arch }} in + aarch64) + pushd /opt/flutter-elinux/flutter + ;; + x86_64) + pushd /opt/flutter + ;; + esac + git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + popd + fi + + # build flutter + pushd /workspace + export CARGO_INCREMENTAL=0 + export DEB_ARCH=${{ matrix.job.deb_arch }} + python3 ./build.py --flutter --skip-cargo + for name in rustdesk*??.deb; do + mv "$name" "${name%%.deb}-${{ matrix.job.arch }}.deb" + done + + # rpm package + echo -e "start packaging fedora package" + pushd /workspace + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + for name in rustdesk*??.rpm; do + mv "$name" /workspace/"${name%%.rpm}.rpm" + done + + # rpm suse package + echo -e "start packaging suse package" + pushd /workspace + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + for name in rustdesk*??.rpm; do + mv "$name" /workspace/"${name%%.rpm}-suse.rpm" + done + + - name: Publish debian/rpm package + if: env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-*.deb + rustdesk-*.rpm + + - name: Upload deb + uses: actions/upload-artifact@master + if: env.UPLOAD_ARTIFACT == 'true' + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb + + # only x86_64 for arch since we can not find newest arm64 docker image to build + # old arch image does not make sense for arch since it is "arch" which always update to date + # and failed to makepkg arm64 on x86_64 + - name: Patch archlinux PKGBUILD + if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' + run: | + sed -i "s/x86_64/${{ matrix.job.arch }}/g" res/PKGBUILD + if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then + sed -i "s/x86_64/aarch64/g" ./res/PKGBUILD + fi + + - name: Build archlinux package + if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' + uses: rustdesk-org/arch-makepkg-action@master + with: + packages: + scripts: | + cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + - name: Publish archlinux package + if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + res/rustdesk-${{ env.VERSION }}*.zst + + build-rustdesk-linux-sciter: + if: ${{ inputs.upload-artifact }} + runs-on: ${{ matrix.job.on }} + name: build-rustdesk-linux-sciter ${{ matrix.job.target }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + on: ubuntu-22.04, + distro: ubuntu18.04, + deb_arch: amd64, + sciter_arch: x64, + vcpkg-triplet: x64-linux, + extra_features: ",hwcodec,unix-file-copy-paste", + } + - { + arch: armv7, + target: armv7-unknown-linux-gnueabihf, + on: ubuntu-22.04-arm, + distro: ubuntu18.04-rustdesk, + deb_arch: armhf, + sciter_arch: arm32, + vcpkg-triplet: arm-linux, + extra_features: ",unix-file-copy-paste", + } + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Modify vcpkg.json for armv7 + if: matrix.job.vcpkg-triplet == 'arm-linux' + run: | + # Replace the baseline in vcpkg.json with ARMV7_VCPKG_COMMIT_ID for armv7 builds + sed -i 's/"baseline": ".*"/"baseline": "${{ env.ARMV7_VCPKG_COMMIT_ID }}"/' vcpkg.json + echo "Modified vcpkg.json for armv7 build:" + grep -A 2 -B 2 '"baseline"' vcpkg.json + + - name: Free Space + run: | + df -h + free -m + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.SCITER_RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - name: Save Rust toolchain version + run: | + RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}') + echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV + + - uses: rustdesk-org/run-on-arch-action@amd64-support + name: Build rustdesk sciter binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ${{ matrix.job.distro }} + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + shell: /bin/bash + install: | + apt-get update + apt-get install -y \ + build-essential \ + clang \ + curl \ + gcc \ + git \ + g++ \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libclang-dev \ + libdbus-1-dev \ + libglib2.0-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + liblzma-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + ninja-build \ + pkg-config \ + python3 \ + python3.7 \ + rpm \ + unzip \ + wget \ + xz-utils \ + zip \ + libssl-dev + # arm-linux needs CMake and vcokg built from source as there + # are no prebuilts available from Kitware and Microsoft + if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then + # install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake + apt-get install -y gcc-8 g++-8 + # bootstrap CMake amd add it to PATH + git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake + pushd /tmp/cmake + ./bootstrap --generator='Unix Makefiles' "--prefix=/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf" + make -j1 install + popd + rm -rf /tmp/cmake + export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH" + fi + # bootstrap vcpkg and set VCPKG_ROOT + export VCPKG_ROOT=/opt/artifacts/vcpkg + mkdir -p /opt/artifacts + pushd /opt/artifacts + rm -rf vcpkg + git clone https://github.com/microsoft/vcpkg + pushd vcpkg + # build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux + if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then + git reset --hard ${{ env.ARMV7_VCPKG_COMMIT_ID }} + CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics + else + git reset --hard ${{ env.VCPKG_COMMIT_ID }} + sh bootstrap-vcpkg.sh -disableMetrics + fi + popd + popd + # rust + pushd /opt + # do not use rustup, because memory overflow in qemu + wget --output-document rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + pushd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} + ./install.sh + popd + rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} + popd + # install newer nasm for aom + wget --output-document nasm.deb "http://ftp.us.debian.org/debian/pool/main/n/nasm/nasm_${{ env.SCITER_NASM_DEBVERSION }}_${{ matrix.job.deb_arch }}.deb" + dpkg -i nasm.deb + rm -f nasm.deb + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # set python3.7 as default python3 + update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 1 + # add built CMake to PATH and set VCPKG_FORCE_SYSTEM_BINARIES Afor arm-linux + if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then + export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH" + export VCPKG_FORCE_SYSTEM_BINARIES=1 + fi + # edit cargo config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + """ > ~/.cargo/config + cat ~/.cargo/config + # install dependencies from vcpkg + export VCPKG_ROOT=/opt/artifacts/vcpkg + # remove this when support higher version + export USE_AOM_391=1 + if ! $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + # build rustdesk + python3 ./res/inline-sciter.py + export CARGO_INCREMENTAL=0 + cargo build --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1 + # make debian package + mkdir -p ./Release + mv ./target/release/rustdesk ./Release/rustdesk + wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/${{ matrix.job.sciter_arch }}/libsciter-gtk.so + export DEB_ARCH=${{ matrix.job.deb_arch }} + ./build.py --package ./Release + + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.deb; do + # use cp to duplicate deb files to fit other packages. + cp "$name" "${name%%.deb}-${{ matrix.job.arch }}-sciter.deb" + done + + - name: Publish debian package + if: env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb + + - name: Upload deb + uses: actions/upload-artifact@master + if: env.UPLOAD_ARTIFACT == 'true' + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb + + build-appimage: + name: Build appimage ${{ matrix.job.target }} + needs: [build-rustdesk-linux] + runs-on: ubuntu-22.04 + if: ${{ inputs.upload-artifact }} + strategy: + fail-fast: false + matrix: + job: + - { target: x86_64-unknown-linux-gnu, arch: x86_64 } + - { target: aarch64-unknown-linux-gnu, arch: aarch64 } + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Download Binary + uses: actions/download-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb + path: . + + - name: Rename Binary + run: | + mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb appimage/rustdesk.deb + + - name: Build appimage package + shell: bash + run: | + # install libarchive-tools for bsdtar command used in AppImageBuilder.yml + sudo apt-get update -y + # https://github.com/AppImage/AppImageKit/wiki/FUSE + sudo apt-get install -y libarchive-tools libfuse2 + # set-up appimage-builder + # https://github.com/AppImage/AppImageKit/issues/1395 + sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml + + - name: Publish appimage package + if: env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage + + build-flatpak: + name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }} + needs: + - build-rustdesk-linux + - build-rustdesk-linux-sciter + runs-on: ${{ matrix.job.on }} + if: ${{ inputs.upload-artifact }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + # https://github.com/ostreedev/ostree/commit/4bac96a8c817beda37448f9b8c662162bb619981 + distro: ubuntu22.04, + on: ubuntu-22.04, + arch: x86_64, + suffix: "", + } + - { + target: x86_64-unknown-linux-gnu, + distro: ubuntu22.04, + on: ubuntu-22.04, + arch: x86_64, + suffix: "-sciter", + } + - { + target: aarch64-unknown-linux-gnu, + # try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub" + distro: ubuntu22.04, + on: ubuntu-22.04-arm, + arch: aarch64, + suffix: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Download Binary + uses: actions/download-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb + path: . + + - name: Rename Binary + run: | + mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb flatpak/rustdesk.deb + + - uses: rustdesk-org/run-on-arch-action@amd64-support + name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + id: flatpak + with: + arch: ${{ matrix.job.arch }} + distro: ${{ matrix.job.distro }} + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + shell: /bin/bash + install: | + apt-get update -y + apt-get install -y git flatpak flatpak-builder + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # flatpak deps + flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + # package + pushd flatpak + git clone https://github.com/flathub/shared-modules.git --depth=1 + flatpak-builder --user --install-deps-from=flathub -y --force-clean --repo=repo ./build ./rustdesk.json + flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk + + - name: Publish flatpak package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak + + build-rustdesk-web: + if: False + name: build-rustdesk-web + runs-on: ubuntu-22.04 + permissions: + contents: read + strategy: + fail-fast: false + env: + RELEASE_NAME: web-basic + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Prepare env + run: | + sudo apt-get update -y + sudo apt-get install -y wget npm + + - name: Install flutter + uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Patch flutter + shell: bash + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + + # https://rustdesk.com/docs/en/dev/build/web/ + - name: Build web + shell: bash + run: | + pushd flutter/web/js + npm install yarn -g + npm install typescript -g + npm install protoc -g + # Install protoc first, see: https://google.github.io/proto-lens/installing-protoc.html + npm install ts-proto + # Only works with vite <= 2.8, see: https://github.com/vitejs/vite/blob/main/docs/guide/build.md#chunking-strategy + npm install vite@2.8 + yarn install && yarn build + popd + + pushd flutter/web + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/web_deps.tar.gz + tar xzf web_deps.tar.gz + popd + + pushd flutter + flutter build web --release + cd build + cp ../web/README.md web + # TODO: Remove the following line when the web is almost complete. + echo -e "\n\nThis build is for preview and not full functionality." >> web/README.md + dir_name="rustdesk-${{ env.VERSION }}-${{ env.RELEASE_NAME }}" + mv web "${dir_name}" && tar czf "${dir_name}".tar.gz "${dir_name}" + sha256sum "${dir_name}".tar.gz + popd + + - name: Publish web + if: env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + flutter/build/rustdesk-${{ env.VERSION }}-${{ env.RELEASE_NAME }}.tar.gz diff --git a/shelled/rustdesk-as-ref/.github/workflows/flutter-ci.yml b/shelled/rustdesk-as-ref/.github/workflows/flutter-ci.yml new file mode 100644 index 0000000..a64dd11 --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/workflows/flutter-ci.yml @@ -0,0 +1,24 @@ +name: Full Flutter CI + +on: + workflow_dispatch: + pull_request: + paths-ignore: + - "docs/**" + - "README.md" + push: + branches: + - master + paths-ignore: + - ".github/**" + - "docs/**" + - "README.md" + - "res/**" + - "appimage/**" + - "flatpak/**" + +jobs: + run-ci: + uses: ./.github/workflows/flutter-build.yml + with: + upload-artifact: false diff --git a/shelled/rustdesk-as-ref/.github/workflows/flutter-nightly.yml b/shelled/rustdesk-as-ref/.github/workflows/flutter-nightly.yml new file mode 100644 index 0000000..b16db4c --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/workflows/flutter-nightly.yml @@ -0,0 +1,15 @@ +name: Flutter Nightly Build + +on: + schedule: + # schedule build every night + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + run-flutter-nightly-build: + uses: ./.github/workflows/flutter-build.yml + secrets: inherit + with: + upload-artifact: true + upload-tag: "nightly" diff --git a/shelled/rustdesk-as-ref/.github/workflows/flutter-tag.yml b/shelled/rustdesk-as-ref/.github/workflows/flutter-tag.yml new file mode 100644 index 0000000..bf39db5 --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/workflows/flutter-tag.yml @@ -0,0 +1,18 @@ +name: Flutter Tag Build + +on: + workflow_dispatch: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+-[0-9]+' + +jobs: + run-flutter-tag-build: + uses: ./.github/workflows/flutter-build.yml + secrets: inherit + with: + upload-artifact: true + upload-tag: ${{ github.ref_name }} \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/.github/workflows/playground.yml b/shelled/rustdesk-as-ref/.github/workflows/playground.yml new file mode 100644 index 0000000..110437e --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/workflows/playground.yml @@ -0,0 +1,418 @@ +name: playground + +on: + #schedule: + # schedule build every night + # - cron: "0/6 * * * *" + workflow_dispatch: + +env: + RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 + CARGO_NDK_VERSION: "3.1.2" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.22.2" + FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "nightly" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" + VERSION: "1.4.6" + NDK_VERSION: "r26d" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + # To make a custom build with your own servers set the below secret values + RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}" + RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}" + API_SERVER: "${{ secrets.API_SERVER }}" + UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}" + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + +jobs: + build-for-macOS: + name: ${{ matrix.job.target }} + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel + extra-build-args: "", + arch: x86_64, + flutter: "3.13.9", + ref: "f6509e3fd6917aa976bad2fc684182601ebf2434", + bridge: "1.80.1", + date: "20231219" + } + - { + target: x86_64-apple-darwin, + os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel + extra-build-args: "", + arch: x86_64, + flutter: "3.10.6", + ref: "f6509e3fd6917aa976bad2fc684182601ebf2434", + bridge: "1.80.1", + date: "20231219" + } + - { + target: x86_64-apple-darwin, + os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel + extra-build-args: "", + arch: x86_64, + flutter: "3.10.6", + ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8", + bridge: "1.80.1", + date: "20231119" + } + - { + target: x86_64-apple-darwin, + os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel + extra-build-args: "", + arch: x86_64, + flutter: "3.13.9", + ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8", + bridge: "1.80.1", + date: "20231119" + } + steps: + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout source code + uses: actions/checkout@v3 + with: + ref: ${{ matrix.job.ref }} + submodules: recursive + + - name: Import the codesign cert + if: env.MACOS_P12_BASE64 != null + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + keychain: rustdesk + + - name: Check sign and import sign key + if: env.MACOS_P12_BASE64 != null + run: | + security default-keychain -s rustdesk.keychain + security find-identity -v + + - name: Import notarize key + if: env.MACOS_P12_BASE64 != null + uses: timheuer/base64-to-file@v1.2 + with: + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling + fileName: rustdesk.json + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} + + - name: Install rcodesign tool + if: env.MACOS_P12_BASE64 != null + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm pkg-config + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ matrix.job.flutter }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + shell: bash + run: | + sed -i '' 's/3.1.0/2.17.0/g' flutter/pubspec.yaml; + cargo install flutter_rust_bridge_codegen --version ${{ matrix.job.bridge }} --features "uuid" --locked + # below works for mac to make buildable on 3.13.9 + # pushd flutter/lib; find . -name "*.dart" | xargs -I{} sed -i '' 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' {}; 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 --c-output ./flutter/macos/Runner/bridge_generated.h + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed" + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + if: false + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + if: false + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus aom + + - name: Show version information (Rust, cargo, Clang) + shell: bash + run: | + clang --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build rustdesk + run: | + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + - name: create unsigned dmg + run: | + CREATE_DMG="$(command -v create-dmg)" + CREATE_DMG="$(readlink -f "$CREATE_DMG")" + sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG" + create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app + + - name: Codesign app and create signed dmg + if: env.MACOS_P12_BASE64 != null + run: | + # Patch create-dmg to give more attempts to unmount image + CREATE_DMG="$(command -v create-dmg)" + CREATE_DMG="$(readlink -f "$CREATE_DMG")" + sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG" + # Unlock keychain + security default-keychain -s rustdesk.keychain + security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain + # start sign the rustdesk.app and dmg + rm -rf *.dmg || true + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv + create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv + # notarize the rustdesk-${{ env.VERSION }}.dmg + rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg + + - name: Rename rustdesk + run: | + for name in rustdesk*??.dmg; do + mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}-flutter${{ matrix.job.flutter }}-flutter${{ matrix.job.date }}.dmg" + done + + - name: Publish DMG package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk*-${{ matrix.job.arch }}*.dmg + + + build-rustdesk-android: + if: false + name: build rustdesk android apk ${{ matrix.job.target }} + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-linux-android, + os: ubuntu-22.04, + openssl-arch: android-arm64, + ref: master, # latest + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + with: + ref: ${{ matrix.job.ref }} + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc-multilib \ + git \ + g++ \ + g++-multilib \ + libayatana-appindicator3-dev\ + libasound2-dev \ + libc6-dev \ + libclang-dev \ + libunwind-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libvdpau-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-dev \ + nasm \ + yasm \ + ninja-build \ + openjdk-11-jdk-headless \ + pkg-config \ + tree \ + wget + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + components: "rustfmt" + + - name: Install flutter rust bridge deps + run: | + git config --global core.longpaths true + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked + sed -i 's/uni_links_desktop/#uni_links_desktop/g' flutter/pubspec.yaml + pushd flutter/lib; find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'; 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 + + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: ${{ env.NDK_VERSION }} + add-to-path: true + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/build_android_deps.sh arm64-v8a + ;; + armv7-linux-androideabi) + ./flutter/build_android_deps.sh armeabi-v7a + ;; + esac + shell: bash + + - name: Clone deps + shell: bash + run: | + pushd /opt + git clone https://github.com/rustdesk-org/rustdesk_thirdparty_lib.git --depth=1 + ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/ + # cp -rf /opt/rustdesk_thirdparty_lib/vcpkg/* /opt/artifacts/vcpkg/ + ls -ls /opt/artifacts/vcpkg/installed/arm64-android/lib/ + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + esac + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + ;; + esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + if: env.ANDROID_SIGNING_KEY != null + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + env: + # override default build-tools version (29.0.3) -- optional + BUILD_TOOLS_VERSION: "30.0.2" + + - name: Publish signed apk package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ${{steps.sign-rustdesk.outputs.signedReleaseFile}} diff --git a/shelled/rustdesk-as-ref/.github/workflows/third-party-RustDeskTempTopMostWindow.yml b/shelled/rustdesk-as-ref/.github/workflows/third-party-RustDeskTempTopMostWindow.yml new file mode 100644 index 0000000..2f89092 --- /dev/null +++ b/shelled/rustdesk-as-ref/.github/workflows/third-party-RustDeskTempTopMostWindow.yml @@ -0,0 +1,60 @@ +name: build RustDeskTempTopMostWindow + +on: + workflow_call: + inputs: + upload-artifact: + type: boolean + default: true + target: + description: 'Target' + required: true + type: string + default: 'windows-2022' + configuration: + description: 'Configuration' + required: true + type: string + default: 'Release' + platform: + description: 'Platform' + required: true + type: string + default: 'x64' + target_version: + description: 'TargetVersion' + required: true + type: string + default: 'Windows10' + +env: + project_path: WindowInjection/WindowInjection.vcxproj + +jobs: + build-RustDeskTempTopMostWindow: + runs-on: ${{ inputs.target }} + strategy: + fail-fast: false + env: + build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }} + steps: + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Download the source code + run: | + git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow + + # Build. commit 53b548a5398624f7149a382000397993542ad796 is tag v0.3 + - name: Build the project + run: | + cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796 + msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }} + + - name: Archive build artifacts + uses: actions/upload-artifact@master + if: ${{ inputs.upload-artifact }} + with: + name: topmostwindow-artifacts + path: | + ./${{ env.build_output_dir }}/WindowInjection.dll diff --git a/shelled/rustdesk-as-ref/.gitignore b/shelled/rustdesk-as-ref/.gitignore new file mode 100644 index 0000000..d2e09a9 --- /dev/null +++ b/shelled/rustdesk-as-ref/.gitignore @@ -0,0 +1,58 @@ +/build +/target +.vscode +.idea +.DS_Store +.env +libsciter-gtk.so +src/ui/inline.rs +extractor +__pycache__ +src/version.rs +*dmg +*exe +*tgz +cert.pfx +*.bak +*png +*svg +*jpg +sciter.dll +**pdb +src/bridge_generated.rs +src/bridge_generated.io.rs +*deb +rustdesk +*.cache +# appimage +appimage/AppDir +appimage/*.AppImage +appimage/appimage-build +appimage/*.xz +# flutter +flutter/linux/build/** +flutter/linux/cmake-build-debug/** +# flatpak +flatpak/.flatpak-builder/** +flatpak/ccache/** +flatpak/.flatpak-builder/build/** +flatpak/.flatpak-builder/shared-modules/** +flatpak/.flatpak-builder/shared-modules/*.tar.xz +flatpak/.flatpak-builder/debian-binary +flatpak/build/** +flatpak/repo/** +flatpak/*.flatpak +# bridge file +lib/generated_bridge.dart +# vscode devcontainer +.gitconfig +.vscode-server/ +.ssh +.devcontainer/.* +# build cache in examples +examples/**/target/ +# === +vcpkg_installed +flutter/lib/generated_plugin_registrant.dart +libsciter.dylib +flutter/web/ \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/.gitmodules b/shelled/rustdesk-as-ref/.gitmodules new file mode 100644 index 0000000..d80e69a --- /dev/null +++ b/shelled/rustdesk-as-ref/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libs/hbb_common"] + path = libs/hbb_common + url = https://github.com/rustdesk/hbb_common diff --git a/shelled/rustdesk-as-ref/CLAUDE.md b/shelled/rustdesk-as-ref/CLAUDE.md new file mode 100644 index 0000000..8d46e1f --- /dev/null +++ b/shelled/rustdesk-as-ref/CLAUDE.md @@ -0,0 +1,91 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Build Commands +- `cargo run` - Build and run the desktop application (requires libsciter library) +- `python3 build.py --flutter` - Build Flutter version (desktop) +- `python3 build.py --flutter --release` - Build Flutter version in release mode +- `python3 build.py --hwcodec` - Build with hardware codec support +- `python3 build.py --vram` - Build with VRAM feature (Windows only) +- `cargo build --release` - Build Rust binary in release mode +- `cargo build --features hwcodec` - Build with specific features + +### Flutter Mobile Commands +- `cd flutter && flutter build android` - Build Android APK +- `cd flutter && flutter build ios` - Build iOS app +- `cd flutter && flutter run` - Run Flutter app in development mode +- `cd flutter && flutter test` - Run Flutter tests + +### Testing +- `cargo test` - Run Rust tests +- `cd flutter && flutter test` - Run Flutter tests + +### Platform-Specific Build Scripts +- `flutter/build_android.sh` - Android build script +- `flutter/build_ios.sh` - iOS build script +- `flutter/build_fdroid.sh` - F-Droid build script + +## Project Architecture + +### Directory Structure +- **`src/`** - Main Rust application code + - `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead) + - `src/server/` - Audio/clipboard/input/video services and network connections + - `src/client.rs` - Peer connection handling + - `src/platform/` - Platform-specific code +- **`flutter/`** - Flutter UI code for desktop and mobile +- **`libs/`** - Core libraries + - `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities + - `libs/scrap/` - Screen capture functionality + - `libs/enigo/` - Platform-specific keyboard/mouse control + - `libs/clipboard/` - Cross-platform clipboard implementation + +### Key Components +- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server +- **Screen Capture**: Platform-specific screen capture in `libs/scrap/` +- **Input Handling**: Cross-platform input simulation in `libs/enigo/` +- **Audio/Video Services**: Real-time audio/video streaming in `src/server/` +- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/` + +### UI Architecture +- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/` +- **Modern UI**: Flutter-based - files in `flutter/` + - Desktop: `flutter/lib/desktop/` + - Mobile: `flutter/lib/mobile/` + - Shared: `flutter/lib/common/` and `flutter/lib/models/` + +## Important Build Notes + +### Dependencies +- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom` +- Set `VCPKG_ROOT` environment variable +- Download appropriate Sciter library for legacy UI support + +### Ignore Patterns +When working with files, ignore these directories: +- `target/` - Rust build artifacts +- `flutter/build/` - Flutter build output +- `flutter/.dart_tool/` - Flutter tooling files + +### Cross-Platform Considerations +- Windows builds require additional DLLs and virtual display drivers +- macOS builds need proper signing and notarization for distribution +- Linux builds support multiple package formats (deb, rpm, AppImage) +- Mobile builds require platform-specific toolchains (Android SDK, Xcode) + +### Feature Flags +- `hwcodec` - Hardware video encoding/decoding +- `vram` - VRAM optimization (Windows only) +- `flutter` - Enable Flutter UI +- `unix-file-copy-paste` - Unix file clipboard support +- `screencapturekit` - macOS ScreenCaptureKit (macOS only) + +### Config +All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types: +- Settings +- Local +- Display +- Built-in diff --git a/shelled/rustdesk-as-ref/Cargo.lock b/shelled/rustdesk-as-ref/Cargo.lock new file mode 100644 index 0000000..febfd6b --- /dev/null +++ b/shelled/rustdesk-as-ref/Cargo.lock @@ -0,0 +1,11288 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if 1.0.0", + "getrandom 0.3.2", + "once_cell", + "version_check", + "zerocopy 0.8.26", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allo-isolate" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b6d794345b06592d0ebeed8e477e41b71e5a0a49df4fc0e4184d5938b99509" +dependencies = [ + "anyhow", + "atomic", + "chrono", + "uuid", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "alsa" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" +dependencies = [ + "alsa-sys", + "bitflags 2.9.1", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.9.1", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk 0.9.0", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum 0.7.2", + "thiserror 1.0.61", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android-wakelock" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90" +dependencies = [ + "jni", + "log", + "ndk-context", +] + +[[package]] +name = "android_log-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" + +[[package]] +name = "android_logger" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f" +dependencies = [ + "android_log-sys", + "env_logger 0.10.2", + "log", + "once_cell", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arboard" +version = "3.4.0" +source = "git+https://github.com/rustdesk-org/arboard#85be1218668ff218a7b170c9d424fde73e069914" +dependencies = [ + "clipboard-win", + "core-graphics 0.23.2", + "image 0.25.1", + "log", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "percent-encoding", + "serde 1.0.228", + "serde_derive", + "windows-sys 0.48.0", + "wl-clipboard-rs", + "x11rb 0.13.1", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits 0.2.19", + "rusticata-macros", + "thiserror 1.0.61", + "time 0.3.36", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "associative-cache" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46016233fc1bb55c23b856fe556b7db6ccd05119a0a392e04f0b3b7c79058f16" + +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-executor" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.1.0", + "futures-lite 2.3.0", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock 2.8.0", + "autocfg 1.3.0", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg 1.3.0", + "cfg-if 1.0.0", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" +dependencies = [ + "async-lock 3.4.0", + "cfg-if 1.0.0", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.7.2", + "rustix 0.38.34", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if 1.0.0", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.34", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "async-signal" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" +dependencies = [ + "async-io 2.3.3", + "async-lock 3.4.0", + "atomic-waker", + "cfg-if 1.0.0", + "futures-core", + "futures-io", + "rustix 0.38.34", + "signal-hook-registry", + "slab", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "atk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +dependencies = [ + "atk-sys", + "glib 0.18.5", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide 0.7.4", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "bindgen" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "clap 2.34.0", + "env_logger 0.9.3", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "rustc-hash 1.1.0", + "shlex", + "which", +] + +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.98", + "which", +] + +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.98", +] + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.98", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "bitmask-enum" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb15541e888071f64592c0b4364fdff21b7cb0a247f984296699351963a8721" +dependencies = [ + "quote 1.0.36", + "syn 2.0.98", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-sys" +version = "0.1.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" +dependencies = [ + "objc-sys 0.2.0-beta.2", +] + +[[package]] +name = "block2" +version = "0.2.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" +dependencies = [ + "block-sys", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite 2.3.0", + "piper", +] + +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "build-target" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytecodec" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325" +dependencies = [ + "byteorder", + "trackable 0.2.24", +] + +[[package]] +name = "bytemuck" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cacao" +version = "0.4.0-beta2" +source = "git+https://github.com/clslaid/cacao?branch=feat/set-file-urls#05e1536b0b43aaae308ec72c0eed703e875b7b95" +dependencies = [ + "bitmask-enum", + "block2 0.2.0-alpha.6", + "core-foundation 0.9.3", + "core-graphics 0.23.1", + "dispatch", + "lazy_static", + "libc", + "objc2 0.3.0-beta.2", + "os_info", + "percent-encoding", + "url", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.9.1", + "cairo-sys-rs", + "glib 0.18.5", + "libc", + "once_cell", + "thiserror 1.0.61", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.9.1", + "log", + "polling 3.7.2", + "rustix 0.38.34", + "slab", + "thiserror 1.0.61", +] + +[[package]] +name = "calloop" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" +dependencies = [ + "bitflags 2.9.1", + "polling 3.7.2", + "rustix 1.1.2", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.34", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.3", + "rustix 1.1.2", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits 0.2.19", + "wasm-bindgen", + "windows-link 0.1.1", +] + +[[package]] +name = "cidr-utils" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2315f7119b7146d6a883de6acd63ddf96071b5f79d9d98d2adaa84d749f6abf1" +dependencies = [ + "debug-helper", + "num-bigint", + "num-traits 0.2.19", + "once_cell", + "regex", +] + +[[package]] +name = "cidre" +version = "0.4.0" +source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf" +dependencies = [ + "cidre-macros", + "parking_lot", +] + +[[package]] +name = "cidre-macros" +version = "0.1.0" +source = "git+https://github.com/yury/cidre.git?rev=f05c428#f05c4288f9870c9fab53272ddafd6ec01c7b2dbf" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.4", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "clipboard" +version = "0.1.0" +dependencies = [ + "cacao", + "cc", + "dashmap 5.5.3", + "dirs 5.0.1", + "fsevent", + "fuser", + "hbb_common", + "lazy_static", + "libc", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "once_cell", + "parking_lot", + "percent-encoding", + "rand 0.8.5", + "serde 1.0.228", + "serde_derive", + "thiserror 1.0.61", + "utf16string", + "uuid", + "x11-clipboard 0.8.1", + "x11rb 0.12.0", + "xattr", +] + +[[package]] +name = "clipboard-master" +version = "4.0.0-beta.6" +source = "git+https://github.com/rustdesk-org/clipboard-master#ddc39f00a6211959489ae683aa6ae6eedf03a809" +dependencies = [ + "objc", + "objc-foundation", + "objc_id", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", + "windows-win", + "wl-clipboard-rs", + "x11-clipboard 0.9.2", + "x11rb 0.13.1", +] + +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "cocoa" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c49e86fc36d5704151f5996b7b3795385f50ce09e3be0f47a0cfde869681cf8" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.7.0", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compression-codecs" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "confy" +version = "0.4.0-2" +source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc5611bb382479" +dependencies = [ + "directories-next", + "serde 1.0.228", + "thiserror 1.0.61", + "toml 0.5.11", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_fn" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d" + +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "unicode-xid 0.2.4", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "constant_time_eq" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "git+https://github.com/madsmtm/core-foundation-rs.git?rev=7d593d016175755e492a92ef89edca68ac3bd5cd#7d593d016175755e492a92ef89edca68ac3bd5cd" +dependencies = [ + "core-foundation-sys 0.8.6", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "git+https://github.com/madsmtm/core-foundation-rs.git?rev=7d593d016175755e492a92ef89edca68ac3bd5cd#7d593d016175755e492a92ef89edca68ac3bd5cd" +dependencies = [ + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.7.0", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.1" +source = "git+https://github.com/madsmtm/core-foundation-rs.git?rev=7d593d016175755e492a92ef89edca68ac3bd5cd#7d593d016175755e492a92ef89edca68ac3bd5cd" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.3", + "core-graphics-types 0.1.2", + "foreign-types 0.5.0", + "libc", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.2" +source = "git+https://github.com/madsmtm/core-foundation-rs.git?rev=7d593d016175755e492a92ef89edca68ac3bd5cd#7d593d016175755e492a92ef89edca68ac3bd5cd" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.3", + "libc", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-media-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273bf3fc5bf51fd06a7766a84788c1540b6527130a0bce39e00567d6ab9f31f1" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-text" +version = "19.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" +dependencies = [ + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "metal", + "objc", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys 0.8.7", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" +dependencies = [ + "bindgen 0.69.4", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#6b374bcaed076750ca8fce6da518ab39b882e14a" +dependencies = [ + "alsa", + "cidre", + "core-foundation-sys 0.8.7", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctor-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctrlc" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" +dependencies = [ + "nix 0.28.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "dart-sys" +version = "4.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895" +dependencies = [ + "cc", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if 1.0.0", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dasp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7381b67da416b639690ac77c73b86a7b5e64a29e31d1f75fb3b1102301ef355a" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_signal", + "dasp_slice", + "dasp_window", +] + +[[package]] +name = "dasp_envelope" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6" +dependencies = [ + "dasp_frame", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", +] + +[[package]] +name = "dasp_frame" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "dasp_interpolate" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_peak" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_ring_buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1" + +[[package]] +name = "dasp_rms" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "dasp_signal" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_window", +] + +[[package]] +name = "dasp_slice" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_window" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi 0.3.9", +] + +[[package]] +name = "dbus-crossroads" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4c83437187544ba5142427746835061b330446ca8902eabd70e4afb8f76de0" +dependencies = [ + "dbus", +] + +[[package]] +name = "debug-helper" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" + +[[package]] +name = "default-net" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4898b43aed56499fad6b294d15b3e76a51df68079bf492e5daae38ca084e003" +dependencies = [ + "dlopen2", + "libc", + "memalloc", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "once_cell", + "system-configuration", + "windows 0.32.0", +] + +[[package]] +name = "default_net" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/default_net#78f8f70cd85151a3a2c4a3230d80d5272703c02e" +dependencies = [ + "anyhow", + "regex", + "winapi 0.3.9", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits 0.2.19", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "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 0.4.5", + "winapi 0.3.9", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.5", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.5", + "winapi 0.3.9", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.4", +] + +[[package]] +name = "dlopen" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" +dependencies = [ + "dlopen_derive", + "lazy_static", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "dlopen2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b121caccfc363e4d9a4589528f3bef7c71b83c6ed01c8dc68cbeeb7fd29ec698" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi 0.3.9", +] + +[[package]] +name = "dlopen2_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a09ac8bb8c16a282264c379dffba707b9c998afc7506009137f3c6136888078" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "dlopen_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" +dependencies = [ + "libc", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "docopt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" +dependencies = [ + "lazy_static", + "regex", + "serde 1.0.228", + "strsim 0.10.0", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" + +[[package]] +name = "drm" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix 0.38.34", +] + +[[package]] +name = "drm-ffi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +dependencies = [ + "drm-sys", + "rustix 0.38.34", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + +[[package]] +name = "dtls" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f531dd7c181beaf3cebab3716afa4d0d41ab888be85232583f56bbaf07ca208a" +dependencies = [ + "aes", + "aes-gcm", + "async-trait", + "bincode", + "byteorder", + "cbc", + "ccm", + "chacha20poly1305", + "der-parser", + "hmac", + "log", + "p256", + "p384", + "portable-atomic", + "rand 0.9.2", + "rand_core 0.6.4", + "rcgen", + "ring", + "rustls", + "sec1", + "serde 1.0.228", + "sha1", + "sha2", + "thiserror 1.0.61", + "tokio", + "webrtc-util", + "x25519-dalek", + "x509-parser", +] + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dylib_virtual_display" +version = "0.1.0" +dependencies = [ + "cc", + "hbb_common", + "lazy_static", + "serde 1.0.228", + "serde_derive", + "thiserror 1.0.61", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", + "spki", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature 1.6.4", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enigo" +version = "0.0.14" +dependencies = [ + "core-graphics 0.22.3", + "hbb_common", + "libxdo-sys", + "log", + "objc", + "pkg-config", + "rdev", + "serde 1.0.228", + "serde_derive", + "tfc", + "unicode-segmentation", + "winapi 0.3.9", +] + +[[package]] +name = "enquote" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c36cb11dbde389f4096111698d8b567c0720e3452fd5ac3e6b4e47e1939932" +dependencies = [ + "thiserror 1.0.61", +] + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "enumflags2" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +dependencies = [ + "enumflags2_derive", + "serde 1.0.228", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "epoll" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74351c3392ea1ff6cd2628e0042d268ac2371cb613252ff383b6dfa50d22fa79" +dependencies = [ + "bitflags 2.9.1", + "libc", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + +[[package]] +name = "evdev" +version = "0.11.5" +source = "git+https://github.com/rustdesk-org/evdev#cec616e37790293d2cd2aa54a96601ed6b1b35a9" +dependencies = [ + "bitvec", + "libc", + "nix 0.23.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide 0.7.4", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset 0.9.1", + "rustc_version", +] + +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "git+https://github.com/rustdesk-org/wezterm?branch=rustdesk/pty_based_0.8.1#80174f8009f41565f0fa8c66dab90d4f9211ae16" +dependencies = [ + "libc", + "thiserror 1.0.61", + "winapi 0.3.9", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "flexi_logger" +version = "0.27.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469e584c031833564840fb0cdbce99bdfe946fd45480a188545e73a76f45461c" +dependencies = [ + "chrono", + "crossbeam-channel", + "crossbeam-queue", + "glob", + "is-terminal", + "lazy_static", + "log", + "nu-ansi-term 0.49.0", + "regex", + "thiserror 1.0.61", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "flutter_rust_bridge" +version = "1.80.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0305ebc9f097d9826530a55fc2acd63222e912c663f7adce3ab641ecc0f346" +dependencies = [ + "allo-isolate", + "anyhow", + "build-target", + "bytemuck", + "cc", + "chrono", + "console_error_panic_hook", + "dart-sys", + "flutter_rust_bridge_macros", + "js-sys", + "lazy_static", + "libc", + "log", + "parking_lot", + "threadpool", + "uuid", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "flutter_rust_bridge_macros" +version = "1.82.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7fe743d921bedf4578b9472346d03a9643a01cd565ca7df7961baebad534ba5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fon" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad46a0e6c9bc688823a742aa969b5c08fdc56c2a436ee00d5c6fbcb5982c55c4" +dependencies = [ + "libm", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fruitbasket" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898289b8e0528c84fb9b88f15ac9d5109bcaf23e0e49bb6f64deee0d86b6a351" +dependencies = [ + "dirs 2.0.2", + "objc", + "objc-foundation", + "objc_id", + "time 0.1.45", +] + +[[package]] +name = "fsevent" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8836d1f147a0a195bf517a5fd211ea7023d19ced903135faf6c4504f2cf8775f" +dependencies = [ + "bitflags 1.3.2", + "fsevent-sys", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "fuser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369" +dependencies = [ + "libc", + "log", + "memchr", + "nix 0.29.0", + "page_size", + "smallvec", + "zerocopy 0.8.26", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand 2.1.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib 0.18.5", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib 0.18.5", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gdk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90fbf5c033c65d93792192a49a8efb5bb1e640c419682a58bb96f5ae77f3d4a" +dependencies = [ + "gdk-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee8f00f4ee46cad2939b8990f5c70c94ff882c3028f3cc5abf950fa4ab53043" +dependencies = [ + "gdk-sys", + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", + "x11 2.21.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "gethostname" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib 0.18.5", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.61", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", + "winapi 0.3.9", +] + +[[package]] +name = "git2" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "glib" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "glib-macros 0.10.1", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "libc", + "once_cell", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.9.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros 0.18.5", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.61", +] + +[[package]] +name = "glib-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" +dependencies = [ + "anyhow", + "heck 0.3.3", + "itertools 0.9.0", + "proc-macro-crate 0.1.5", + "proc-macro-error", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "glib-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" +dependencies = [ + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gobject-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" +dependencies = [ + "glib-sys 0.10.1", + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "gstreamer" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.0", + "futures-channel", + "futures-core", + "futures-util", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer-sys", + "libc", + "muldiv", + "num-rational", + "once_cell", + "paste", + "pretty-hex", + "thiserror 1.0.61", +] + +[[package]] +name = "gstreamer-app" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc80888271338c3ede875d8cafc452eb207476ff5539dcbe0018a8f5b827af0e" +dependencies = [ + "bitflags 1.3.2", + "futures-core", + "futures-sink", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer", + "gstreamer-app-sys", + "gstreamer-base", + "gstreamer-sys", + "libc", + "once_cell", +] + +[[package]] +name = "gstreamer-app-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "813f64275c9e7b33b828b9efcf9dfa64b95996766d4de996e84363ac65b87e3d" +dependencies = [ + "glib-sys 0.10.1", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "gstreamer-base" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafd01c56f59cb10f4b5a10f97bb4bdf8c2b2784ae5b04da7e2d400cf6e6afcf" +dependencies = [ + "bitflags 1.3.2", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b7b6dc2d6e160a1ae28612f602bd500b3fa474ce90bf6bb2f08072682beef5" +dependencies = [ + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer-sys", + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "gstreamer-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1f154082d01af5718c5f8a8eb4f565a4ea5586ad8833a8fc2c2aa6844b601d" +dependencies = [ + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "gstreamer-video" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7bbb1485d87469849ec45c08e03c2f280d3ea20ff3c439d03185be54e3ce98e" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-util", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer", + "gstreamer-base", + "gstreamer-base-sys", + "gstreamer-sys", + "gstreamer-video-sys", + "libc", + "once_cell", +] + +[[package]] +name = "gstreamer-video-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92347e46438007d6a2386302125f62cb9df6769cdacb931af5c0f12c1ee21de4" +dependencies = [ + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "gtk" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib 0.18.5", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pango-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if 1.0.0", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "hbb_common" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-recursion", + "backtrace", + "base64 0.22.1", + "bytes", + "chrono", + "clap 4.5.53", + "confy", + "default_net", + "directories-next", + "dirs-next", + "dlopen", + "env_logger 0.11.6", + "filetime", + "flexi_logger", + "futures", + "futures-util", + "httparse", + "lazy_static", + "libc", + "libloading 0.8.4", + "log", + "mac_address", + "machine-uid", + "osascript", + "protobuf", + "protobuf-codegen", + "rand 0.8.5", + "regex", + "rustls-native-certs", + "rustls-pki-types", + "rustls-platform-verifier", + "serde 1.0.228", + "serde_derive", + "serde_json 1.0.118", + "sha2", + "smithay-client-toolkit 0.20.0", + "socket2 0.3.19", + "sodiumoxide", + "sysinfo", + "thiserror 1.0.61", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-socks", + "tokio-tungstenite", + "tokio-util", + "toml 0.7.8", + "tungstenite", + "url", + "users 0.11.0", + "uuid", + "webpki-roots 1.0.4", + "webrtc", + "whoami", + "winapi 0.3.9", + "x11 2.21.0", + "zstd 0.13.1", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.11", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hwcodec" +version = "0.7.1" +source = "git+https://github.com/rustdesk-org/hwcodec#398e5a8938dd8768ade0fcdc27ea80e8b4b38738" +dependencies = [ + "bindgen 0.59.2", + "cc", + "log", + "serde 1.0.228", + "serde_derive", + "serde_json 1.0.118", +] + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa 1.0.11", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.4", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys 0.8.7", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits 0.2.19", + "png 0.17.13", + "qoi", + "tiff", +] + +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "num-traits 0.2.19", + "png 0.17.13", + "tiff", +] + +[[package]] +name = "impersonate_system" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167" +dependencies = [ + "cc", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "interceptor" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea51375727680dc15f06e8ad90fa31df75d79dd030100e8ad60eef1c27fe2c98" +dependencies = [ + "async-trait", + "bytes", + "futures", + "log", + "portable-atomic", + "rand 0.9.2", + "rtcp", + "rtp", + "thiserror 1.0.61", + "tokio", + "waitgroup", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ioctl-rs" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde 1.0.228", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.0", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_debug" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if 1.0.0", + "combine", + "jni-sys", + "log", + "thiserror 1.0.61", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kcp-sys" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/kcp-sys#32a6c09fc6223f54aea83981a6aa8995931d29be" +dependencies = [ + "anyhow", + "auto_impl", + "bindgen 0.71.1", + "bitflags 2.9.1", + "bytes", + "cc", + "dashmap 6.1.0", + "log", + "parking_lot", + "rand 0.8.5", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "zerocopy 0.7.34", +] + +[[package]] +name = "keepawake" +version = "0.4.3" +source = "git+https://github.com/rustdesk-org/keepawake-rs#64d568586dd16551d02120e19668d2b0fec8e3c9" +dependencies = [ + "anyhow", + "cfg-if 1.0.0", + "core-foundation 0.9.4", + "shadow-rs", + "windows 0.48.0", + "winres", + "zbus", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.1", + "serde 1.0.228", + "unicode-segmentation", +] + +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib 0.18.5", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libgit2-sys" +version = "0.14.2+1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if 1.0.0", + "winapi 0.3.9", +] + +[[package]] +name = "libloading" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +dependencies = [ + "cfg-if 1.0.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libpulse-binding" +version = "2.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libpulse-sys", + "num-derive 0.3.3", + "num-traits 0.2.19", + "winapi 0.3.9", +] + +[[package]] +name = "libpulse-simple-binding" +version = "2.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05fd6b68f33f6a251265e6ed1212dc3107caad7c5c6fdcd847b2e65ef58c308d" +dependencies = [ + "libpulse-binding", + "libpulse-simple-sys", + "libpulse-sys", +] + +[[package]] +name = "libpulse-simple-sys" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6613b4199d8b9f0edcfb623e020cb17bbd0bee8dd21f3c7cc938de561c4152" +dependencies = [ + "libpulse-sys", + "pkg-config", +] + +[[package]] +name = "libpulse-sys" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" +dependencies = [ + "libc", + "num-derive 0.3.3", + "num-traits 0.2.19", + "pkg-config", + "winapi 0.3.9", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.1", + "libc", + "redox_syscall 0.5.2", +] + +[[package]] +name = "libsamplerate-sys" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28853b399f78f8281cd88d333b54a63170c4275f6faea66726a2bea5cca72e0d" +dependencies = [ + "cmake", +] + +[[package]] +name = "libsodium-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "walkdir", +] + +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +dependencies = [ + "hbb_common", +] + +[[package]] +name = "libz-sys" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "line-wrap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg 1.3.0", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac_address" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8836fae9d0d4be2c8b4efcdd79e828a2faa058a90d005abf42f91cac5493a08e" +dependencies = [ + "nix 0.28.0", + "winapi 0.3.9", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "machine-uid" +version = "0.3.0" +source = "git+https://github.com/rustdesk-org/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542" +dependencies = [ + "bindgen 0.59.2", + "cc", + "winreg 0.11.0", +] + +[[package]] +name = "magnum-opus" +version = "0.4.0" +source = "git+https://github.com/rustdesk-org/magnum-opus#5cd2bf989c148662fa3a2d9d539a71d71fd1d256" +dependencies = [ + "bindgen 0.59.2", + "pkg-config", + "target_build_utils", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if 1.0.0", + "digest", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memalloc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "metal" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e198a0ee42bdbe9ef2c09d0b9426f3b2b47d90d93a4a9b0395c4cea605e92dc0" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa 0.20.2", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "log", + "objc", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mozjpeg" +version = "0.10.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55571bce4f12d80ceb4296526e7614f796df72daaaac85f265ab732fa47b7bc9" +dependencies = [ + "arrayvec", + "bytemuck", + "libc", + "mozjpeg-sys", + "rgb", +] + +[[package]] +name = "mozjpeg-sys" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3626d7942d5b56cc6d47b1c59724c0a976b786fca059c5aaa904aef6324d55" +dependencies = [ + "cc", + "dunce", + "libc", + "nasm-rs", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png 0.17.13", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "muldiv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "nasm-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fcfa1bd49e0342ec1d07ed2be83b59963e7acbeb9310e1bb2c07b69dadd959" +dependencies = [ + "jobserver", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.10.0", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "native-windows-gui" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7003a669f68deb6b7c57d74fff4f8e533c44a3f0b297492440ef4ff5a28454" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "winapi 0.3.9", + "winapi-build", +] + +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys 0.4.1+23.1.7779620", + "num_enum 0.5.11", + "raw-window-handle 0.5.2", + "thiserror 1.0.61", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum 0.7.2", + "thiserror 1.0.61", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum 0.7.2", + "raw-window-handle 0.6.2", + "thiserror 1.0.61", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "netlink-packet-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5cf0b54effda4b91615c40ff0fd12d0d4c9a6e0f5116874f03941792ff535a" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea993e32c77d87f01236c38f572ecb6c311d592e56a06262a007fd2a6e31253c" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.61", +] + +[[package]] +name = "netlink-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416060d346fbaf1f23f9512963e3e878f1a78e707cb699ba9215761754244307" +dependencies = [ + "bytes", + "libc", + "log", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg 1.3.0", + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", + "pin-utils", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.0", + "cfg_aliases 0.1.1", + "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.0", + "cfg_aliases 0.2.1", + "libc", +] + +[[package]] +name = "nokhwa" +version = "0.10.7" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "flume", + "image 0.25.1", + "nokhwa-bindings-linux", + "nokhwa-bindings-macos", + "nokhwa-bindings-windows", + "nokhwa-core", + "paste", + "thiserror 2.0.17", +] + +[[package]] +name = "nokhwa-bindings-linux" +version = "0.1.1" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "nokhwa-core", + "v4l", +] + +[[package]] +name = "nokhwa-bindings-macos" +version = "0.2.2" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-media-sys", + "core-video-sys", + "flume", + "nokhwa-core", + "objc", + "once_cell", +] + +[[package]] +name = "nokhwa-bindings-windows" +version = "0.4.2" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "dlopen", + "lazy_static", + "nokhwa-core", + "once_cell", + "windows 0.43.0", +] + +[[package]] +name = "nokhwa-core" +version = "0.1.5" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "bytes", + "image 0.25.1", + "mozjpeg", + "thiserror 2.0.17", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi 0.3.9", +] + +[[package]] +name = "nu-ansi-term" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg 1.3.0", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive 0.7.2", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro-crate 2.0.2", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.2.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.3.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a49f420f16c8814efdcd6b4258664de9d9920cbc26b6f95d034a1ca9850ccc2c" +dependencies = [ + "block2 0.2.0-alpha.6", + "objc-sys 0.2.0-beta.2", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys 0.3.5", + "objc2-encode 4.1.0", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode 4.1.0", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.9.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "2.0.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" +dependencies = [ + "objc-sys 0.2.0-beta.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +dependencies = [ + "memchr", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk 0.8.0", + "ndk-context", + "num-derive 0.4.2", + "num-traits 0.2.19", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.0", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.5.3+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6bad8cd0233b63971e232cc9c5e83039375b8586d2312f31fda85db8f888c2" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libredox", +] + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os-version" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8a1fed76ac765e39058ca106b6229a93c5a60292a1bd4b602ce2be11e1c020" +dependencies = [ + "anyhow", + "plist", + "uname", + "winapi 0.3.9", +] + +[[package]] +name = "os_info" +version = "3.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +dependencies = [ + "log", + "serde 1.0.228", + "windows-sys 0.52.0", +] + +[[package]] +name = "os_pipe" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "osascript" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" +dependencies = [ + "serde 1.0.228", + "serde_derive", + "serde_json 1.0.118", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "pam" +version = "0.7.0" +source = "git+https://github.com/rustdesk-org/pam#7bfd25510202cd269292cbdd7c71f3977a6fd762" +dependencies = [ + "libc", + "pam-macros", + "pam-sys", + "users 0.10.0", +] + +[[package]] +name = "pam-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "pam-sys" +version = "1.0.0-alpha4" +source = "git+https://github.com/rustdesk-org/pam-sys?branch=fix/v1.0.0-alpha4_gnuc_va_list#3337c9bb9a9c68d7497ec8c93cad2368c26091b7" +dependencies = [ + "bindgen 0.59.2", + "libc", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib 0.18.5", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "parity-tokio-ipc" +version = "0.7.3-5" +source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291" +dependencies = [ + "futures", + "libc", + "log", + "rand 0.8.5", + "tokio", + "winapi 0.3.9", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.5.2", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_shared 0.7.24", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +dependencies = [ + "phf_generator 0.7.24", + "phf_shared 0.7.24", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared 0.7.24", + "rand 0.6.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher 0.2.3", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "piet" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e381186490a3e2017a506d62b759ea8eaf4be14666b13ed53973e8ae193451b1" +dependencies = [ + "kurbo", + "unic-bidi", +] + +[[package]] +name = "piet-coregraphics" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819b41d2ddb1d8abf3e45e49422f866cba281b4abb5e2fb948bba06e2c3d3f7" +dependencies = [ + "associative-cache", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.7", + "core-graphics 0.22.3", + "core-text", + "foreign-types 0.3.2", + "piet", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" +dependencies = [ + "atomic-waker", + "fastrand 2.1.0", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "plist" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +dependencies = [ + "base64 0.21.7", + "indexmap", + "line-wrap", + "quick-xml 0.31.0", + "serde 1.0.228", + "time 0.3.36", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.4", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.9.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg 1.3.0", + "bitflags 1.3.2", + "cfg-if 1.0.0", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +dependencies = [ + "cfg-if 1.0.0", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-pty" +version = "0.8.1" +source = "git+https://github.com/rustdesk-org/wezterm?branch=rustdesk/pty_based_0.8.1#80174f8009f41565f0fa8c66dab90d4f9211ae16" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix 0.25.1", + "serial", + "shared_library", + "shell-words", + "winapi 0.3.9", + "winreg 0.10.1", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pretty-hex" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" + +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2 1.0.93", + "syn 2.0.98", +] + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror 1.0.61", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.61", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.61", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.61", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "qrcode-generator" +version = "4.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d06cb9646c7a14096231a2474d7f21e5e8c13de090c68d13bde6157cfe7f159" +dependencies = [ + "html-escape", + "image 0.24.9", + "qrcodegen", +] + +[[package]] +name = "qrcodegen" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" + +[[package]] +name = "quest" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556af5f5c953a2ee13f45753e581a38f9778e6551bc3ccc56d90b14628fe59d8" +dependencies = [ + "cfg-if 0.1.10", + "rpassword 2.1.0", + "tempfile", + "termios 0.3.3", + "winapi 0.3.9", +] + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases 0.2.1", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2 1.0.93", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time 0.3.36", + "x509-parser", + "yasna", +] + +[[package]] +name = "rdev" +version = "0.5.0-2" +source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8" +dependencies = [ + "cocoa 0.24.1", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.7", + "core-graphics 0.22.3", + "dispatch", + "enum-map", + "epoll", + "inotify", + "lazy_static", + "libc", + "log", + "mio 0.8.11", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring", + "winapi 0.3.9", + "x11 2.21.0", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "realfft" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953d9f7e5cdd80963547b456251296efc2626ed4e3cbf36c869d9564e0220571" +dependencies = [ + "rustfft", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.61", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "remote_printer" +version = "0.1.0" +dependencies = [ + "hbb_common", + "winapi 0.3.9", + "windows-strings 0.3.1", +] + +[[package]] +name = "repng" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd57cd2cb5cc699b3eb4824d654e5a32f3bc013766da4966f71fe94805abbda" +dependencies = [ + "byteorder", + "flate2", +] + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "async-compression", + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde 1.0.228", + "serde_json 1.0.118", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.4", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ringbuf" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79abed428d1fd2a128201cec72c5f6938e2da607c6f3745f769fabea399d950a" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rpassword" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37473170aedbe66ffa3ad3726939ba677d83c646ad4fd99e5b4bc38712f45ec" +dependencies = [ + "kernel32-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtcp" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d30d1c4091644431c22acf9f8be6191b56805e0e977f15ca7104b4a6d6eaec" +dependencies = [ + "bytes", + "thiserror 1.0.61", + "webrtc-util", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtp" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f126f38ea84c02480e32e547c1459a939052f74fb92117ac3eef23fdac6b023" +dependencies = [ + "bytes", + "memchr", + "portable-atomic", + "rand 0.9.2", + "serde 1.0.228", + "thiserror 1.0.61", + "webrtc-util", +] + +[[package]] +name = "rubato" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd70209c27d5b08f5528bdc779ea3ffb418954e28987f9f9775c6eac41003f9c" +dependencies = [ + "num-complex", + "num-integer", + "num-traits 0.2.19", + "realfft", +] + +[[package]] +name = "runas" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96d6b6c505282b007a9b009f2aa38b2fd0359b81a0430ceacc60f69ade4c6a0" +dependencies = [ + "libc", + "security-framework-sys", + "which", + "windows-sys 0.48.0", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if 1.0.0", + "ordered-multimap", +] + +[[package]] +name = "rust-pulsectl" +version = "0.2.12" +source = "git+https://github.com/rustdesk-org/pulsectl#aa34dde499aa912a3abc5289cc0b547bd07dd6e2" +dependencies = [ + "libpulse-binding", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustdesk" +version = "1.4.6" +dependencies = [ + "android-wakelock", + "android_logger", + "arboard", + "async-process", + "async-trait", + "bytemuck", + "bytes", + "cc", + "cfg-if 1.0.0", + "chrono", + "cidr-utils", + "clap 4.5.53", + "clipboard", + "clipboard-master", + "cocoa 0.24.1", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "cpal", + "crossbeam-queue", + "ctrlc", + "dasp", + "dbus", + "dbus-crossroads", + "default-net", + "dispatch", + "docopt", + "enigo", + "errno", + "evdev", + "flutter_rust_bridge", + "fon", + "fontdb", + "foreign-types 0.3.2", + "fruitbasket", + "gtk", + "hbb_common", + "hex", + "hound", + "image 0.24.9", + "impersonate_system", + "include_dir", + "jni", + "kcp-sys", + "keepawake", + "lazy_static", + "libpulse-binding", + "libpulse-simple-binding", + "libxdo-sys", + "mac_address", + "magnum-opus", + "nix 0.29.0", + "num_cpus", + "objc", + "objc_id", + "once_cell", + "openssl", + "os-version", + "pam", + "parity-tokio-ipc", + "percent-encoding", + "piet", + "piet-coregraphics", + "portable-pty", + "qrcode-generator", + "rdev", + "remote_printer", + "repng", + "reqwest", + "ringbuf", + "rpassword 7.3.1", + "rubato", + "runas", + "rust-pulsectl", + "samplerate", + "sciter-rs", + "scrap", + "serde 1.0.228", + "serde_derive", + "serde_json 1.0.118", + "serde_repr", + "sha2", + "shared_memory", + "shutdown_hooks", + "softbuffer", + "stunclient", + "sys-locale", + "system_shutdown", + "tao", + "tauri-winrt-notification", + "terminfo", + "termios 0.3.3", + "tiny-skia", + "totp-rs", + "tray-icon", + "ttf-parser", + "url", + "uuid", + "virtual_display", + "wallpaper", + "winapi 0.3.9", + "windows 0.61.1", + "windows-service", + "winit", + "winreg 0.11.0", + "winres", + "wol-rs", + "x11-clipboard 0.8.1", + "x11rb 0.12.0", + "zip", +] + +[[package]] +name = "rustdesk-portable-packer" +version = "1.4.6" +dependencies = [ + "brotli", + "dirs 5.0.1", + "md5", + "native-windows-gui", + "winapi 0.3.9", + "windows 0.61.1", + "winres", +] + +[[package]] +name = "rustfft" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86" +dependencies = [ + "num-complex", + "num-integer", + "num-traits 0.2.19", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys 0.8.7", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "samplerate" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e032b2b24715c4f982f483ea3abdb3c9ba444d9f63e87b2843d6f998f5ba2698" +dependencies = [ + "libsamplerate-sys", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "sciter-rs" +version = "0.5.57" +source = "git+https://github.com/rustdesk-org/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821" +dependencies = [ + "lazy_static", + "libc", + "objc", + "objc-foundation", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrap" +version = "0.5.0" +dependencies = [ + "android_logger", + "bindgen 0.65.1", + "block", + "cfg-if 1.0.0", + "dbus", + "docopt", + "gstreamer", + "gstreamer-app", + "gstreamer-video", + "hbb_common", + "hwcodec", + "jni", + "lazy_static", + "log", + "ndk 0.7.0", + "ndk-context", + "nokhwa", + "num_cpus", + "pkg-config", + "quest", + "repng", + "serde 1.0.228", + "serde_json 1.0.118", + "target_build_utils", + "tracing", + "webm", + "winapi 0.3.9", + "zbus", +] + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + +[[package]] +name = "sdp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c374dceda16965d541c8800ce9cc4e1c14acfd661ddf7952feeedc3411e5c6" +dependencies = [ + "rand 0.9.2", + "substring", + "thiserror 1.0.61", + "url", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.7", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-foundation-sys 0.8.7", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "serde_json" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8bcf487be7d2e15d3d543f04312de991d631cfe1b43ea0ade69e6a8a5b16a1" +dependencies = [ + "dtoa", + "itoa 0.3.4", + "num-traits 0.1.43", + "serde 0.9.15", +] + +[[package]] +name = "serde_json" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +dependencies = [ + "itoa 1.0.11", + "ryu", + "serde 1.0.228", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.11", + "ryu", + "serde 1.0.228", +] + +[[package]] +name = "serial" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86" +dependencies = [ + "serial-core", + "serial-unix", + "serial-windows", +] + +[[package]] +name = "serial-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581" +dependencies = [ + "libc", +] + +[[package]] +name = "serial-unix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7" +dependencies = [ + "ioctl-rs", + "libc", + "serial-core", + "termios 0.2.2", +] + +[[package]] +name = "serial-windows" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162" +dependencies = [ + "libc", + "serial-core", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[package]] +name = "shadow-rs" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427f07ab5f873000cf55324882e12a88c0a7ea7025df4fc1e7e35e688877a583" +dependencies = [ + "const_format", + "git2", + "is_debug", + "time 0.3.36", + "tzdb 0.5.10", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shared_memory" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8593196da75d9dc4f69349682bd4c2099f8cde114257d1ef7ef1b33d1aba54" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "nix 0.23.2", + "rand 0.8.5", + "win-sys", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shutdown_hooks" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6057adedbec913419c92996f395ba69931acbd50b7d56955394cd3f7bedbfa45" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.9.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.34", + "thiserror 1.0.61", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.9.1", + "calloop 0.14.3", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.2", + "thiserror 2.0.17", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "sodiumoxide" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" +dependencies = [ + "ed25519", + "libc", + "libsodium-sys", + "serde 1.0.228", +] + +[[package]] +name = "softbuffer" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases 0.2.1", + "core-graphics 0.23.2", + "drm", + "fastrand 2.1.0", + "foreign-types 0.5.0", + "js-sys", + "log", + "memmap2", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core", + "raw-window-handle 0.6.2", + "redox_syscall 0.5.2", + "rustix 0.38.34", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.52.0", + "x11rb 0.13.1", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +dependencies = [ + "heck 0.3.3", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.93", + "quote 1.0.36", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "stun" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a512c5d501e3e3b5a4bb3e8e31462d56d54a66b95a28b8596e14422bf21c32b" +dependencies = [ + "base64 0.22.1", + "crc", + "lazy_static", + "md-5", + "rand 0.9.2", + "ring", + "subtle", + "thiserror 1.0.61", + "tokio", + "url", + "webrtc-util", +] + +[[package]] +name = "stun_codec" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feed9dafe0bda84f2b6ca3ce726b0a1f1ac2e8b63c6ecfb89b08b32313247b5b" +dependencies = [ + "bytecodec", + "byteorder", + "crc", + "hmac", + "md5", + "sha1", + "trackable 1.3.0", +] + +[[package]] +name = "stunclient" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c969a14b4a4c09c320416ebf880b3d5a81ad1612065741eb10521951c06c8991" +dependencies = [ + "bytecodec", + "rand 0.8.5", + "stun_codec", + "tokio", +] + +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.29.10" +source = "git+https://github.com/rustdesk-org/sysinfo?branch=rlim_max#90b1705d909a4902dbbbdea37ee64db17841077d" +dependencies = [ + "cfg-if 1.0.0", + "core-foundation-sys 0.8.7", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows 0.51.1", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "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.7", + "libc", +] + +[[package]] +name = "system-deps" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" +dependencies = [ + "heck 0.3.3", + "pkg-config", + "strum 0.18.0", + "strum_macros 0.18.0", + "thiserror 1.0.61", + "toml 0.5.11", + "version-compare 0.0.10", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare 0.2.0", +] + +[[package]] +name = "system_shutdown" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7567f71160af5e9abfb4f5a21532cf2174cefe91ac5c336419295685a695cc66" +dependencies = [ + "windows 0.44.0", + "zbus", +] + +[[package]] +name = "tao" +version = "0.25.0" +source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cocoa 0.25.0", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "crossbeam-channel", + "dispatch", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "image 0.24.9", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk 0.7.0", + "ndk-context", + "ndk-sys 0.4.1+23.1.7779620", + "objc", + "once_cell", + "parking_lot", + "png 0.17.13", + "raw-window-handle 0.6.2", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.52.0", + "windows-implement 0.52.0", + "windows-version", + "x11-dl", + "zbus", +] + +[[package]] +name = "tao-macros" +version = "0.1.2" +source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + +[[package]] +name = "target_build_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "013d134ae4a25ee744ad6129db589018558f620ddfa44043887cdd45fa08e75c" +dependencies = [ + "phf 0.7.24", + "phf_codegen 0.7.24", + "serde_json 0.9.10", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006851c9ccefa3c38a7646b8cec804bb429def3da10497bfa977179869c3e8e2" +dependencies = [ + "quick-xml 0.30.0", + "windows 0.51.1", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if 1.0.0", + "fastrand 2.1.0", + "rustix 0.38.34", + "windows-sys 0.52.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminfo" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" +dependencies = [ + "dirs 4.0.0", + "fnv", + "nom", + "phf 0.11.3", + "phf_codegen 0.11.3", +] + +[[package]] +name = "termios" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" +dependencies = [ + "libc", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "tfc" +version = "0.7.0" +source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#78bb80a8e596e4c14ae57c8448f5fca75f91f2b0" +dependencies = [ + "anyhow", + "core-graphics 0.23.2", + "unicode-segmentation", + "winapi 0.3.9", + "x11 2.19.0", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl 1.0.61", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa 1.0.11", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde 1.0.228", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if 1.0.0", + "log", + "png 0.17.13", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading 0.8.4", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinyvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio 1.0.3", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.10", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2-3" +source = "git+https://github.com/rustdesk-org/tokio-socks#bdb9aa3de5bac41602d0742b8ef6bbc6bfebd127" +dependencies = [ + "bytes", + "either", + "futures-core", + "futures-sink", + "futures-util", + "pin-project", + "thiserror 2.0.17", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.9", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "futures-util", + "hashbrown 0.15.4", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde 1.0.228", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde 1.0.228", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde 1.0.228", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde 1.0.228", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "totp-rs" +version = "5.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c4ae9724c5888c0417d2396037ed3b60665925624766416e3e342b6ba5dbd3f" +dependencies = [ + "base32", + "constant_time_eq 0.2.6", + "hmac", + "rand 0.8.5", + "sha1", + "sha2", + "url", + "urlencoding", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term 0.46.0", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trackable" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" +dependencies = [ + "trackable 1.3.0", + "trackable_derive", +] + +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "git+https://github.com/tauri-apps/tray-icon#0a5835b0e6828e37a1f781de9c2d671ae7a939e6" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png 0.18.1", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "tree_magic_mini" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469a727cac55b41448315cc10427c069c618ac59bb6a4480283fcd811749bdc2" +dependencies = [ + "fnv", + "home", + "memchr", + "nom", + "once_cell", + "petgraph", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "sha1", + "thiserror 2.0.17", + "utf-8", + "webpki-roots 0.26.9", +] + +[[package]] +name = "turn" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed995882f66ab94238de77c62e5e778389698ab700afa4696f4754da8f457cb" +dependencies = [ + "async-trait", + "base64 0.22.1", + "futures", + "log", + "md-5", + "portable-atomic", + "rand 0.9.2", + "ring", + "stun", + "thiserror 1.0.61", + "tokio", + "tokio-util", + "webrtc-util", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "tz-rs" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33851b15c848fad2cf4b105c6bb66eb9512b6f6c44a4b13f57c53c73c707e2b4" +dependencies = [ + "const_fn", +] + +[[package]] +name = "tzdb" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a18ee5bde3433d683d41859650804a5ad89cad17f153a53f1e6a96e0da2d969" +dependencies = [ + "iana-time-zone", + "tz-rs", + "tzdb 0.6.1", +] + +[[package]] +name = "tzdb" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b580f6b365fa89f5767cdb619a55d534d04a4e14c2d7e5b9a31e94598687fb1" +dependencies = [ + "iana-time-zone", + "tz-rs", + "tzdb_data", +] + +[[package]] +name = "tzdb_data" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1889fdffac09d65c1d95c42d5202e9b21ad8c758f426e9fe09088817ea998d6" +dependencies = [ + "tz-rs", +] + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "winapi 0.3.9", +] + +[[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + +[[package]] +name = "unic-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1356b759fb6a82050666f11dce4b6fe3571781f1449f3ef78074e408d468ec09" +dependencies = [ + "matches", + "unic-ucd-bidi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d568b51222484e1f8209ce48caa6b430bf352962b877d592c29ab31fb53d8c" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde 1.0.228", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "users" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b62a1e85e12d5d712bf47a85f426b73d303e2d00a90de5f3004df3596e9d216" +dependencies = [ + "byteorder", +] + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "v4l" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fbfea44a46799d62c55323f3c55d06df722fbe577851d848d328a1041c3403" +dependencies = [ + "bitflags 1.3.2", + "libc", + "v4l2-sys-mit", +] + +[[package]] +name = "v4l2-sys-mit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6779878362b9bacadc7893eac76abe69612e8837ef746573c4a5239daf11990b" +dependencies = [ + "bindgen 0.65.1", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version-compare" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "virtual_display" +version = "0.1.0" +dependencies = [ + "hbb_common", + "lazy_static", +] + +[[package]] +name = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wallpaper" +version = "3.2.0" +source = "git+https://github.com/rustdesk-org/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf" +dependencies = [ + "dirs 5.0.1", + "enquote", + "rust-ini", + "thiserror 1.0.61", + "winapi 0.3.9", + "winreg 0.11.0", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote 1.0.36", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.2", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.1", + "rustix 1.1.2", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.9.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +dependencies = [ + "rustix 0.38.34", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfe33d551eb8bffd03ff067a8b44bb963919157841a99957151299a6307d19c" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79f2d57c7fcc6ab4d602adba364bf59a5c24de57bd194486bf9b8360e06bfc4" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2 1.0.93", + "quick-xml 0.37.5", + "quote 1.0.36", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webm" +version = "1.1.0" +source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" +dependencies = [ + "webm-sys", +] + +[[package]] +name = "webm-sys" +version = "1.0.4" +source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" +dependencies = [ + "cc", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d651ec480de84b762e7be71e6efa7461699c19d9e2c272c8d93455f567786e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29aad86cec885cafd03e8305fd727c418e970a521322c91688414d5b8efba16b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webrtc" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08fd686c0920ac08f3a57eacc48e31f0e4ca1ffefba4478784606f78c14e83ad" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "dtls", + "hex", + "interceptor", + "lazy_static", + "log", + "portable-atomic", + "rand 0.9.2", + "rcgen", + "regex", + "ring", + "rtcp", + "rtp", + "sdp", + "serde 1.0.228", + "serde_json 1.0.118", + "sha2", + "smol_str", + "stun", + "thiserror 1.0.61", + "tokio", + "turn", + "unicase", + "url", + "waitgroup", + "webrtc-data", + "webrtc-ice", + "webrtc-mdns", + "webrtc-media", + "webrtc-sctp", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "webrtc-data" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062a5438d63bb0756a221693d76cc0dd6119affee1dfdfe57abe3a2a8c8b3eea" +dependencies = [ + "bytes", + "log", + "portable-atomic", + "thiserror 1.0.61", + "tokio", + "webrtc-sctp", + "webrtc-util", +] + +[[package]] +name = "webrtc-ice" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cb13fd1a373e68addc4bba0c8ca058627518e54342583d024bdcbb8ae5d97d" +dependencies = [ + "arc-swap", + "async-trait", + "crc", + "log", + "portable-atomic", + "rand 0.9.2", + "serde 1.0.228", + "serde_json 1.0.118", + "stun", + "thiserror 1.0.61", + "tokio", + "turn", + "url", + "uuid", + "waitgroup", + "webrtc-mdns", + "webrtc-util", +] + +[[package]] +name = "webrtc-mdns" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17279a067e75df72ce923fdeb7f04cd808f6f5aa4910dc6bcb4fbe66b396ace" +dependencies = [ + "log", + "socket2 0.5.10", + "thiserror 1.0.61", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-media" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a84c910fec0848fd5a0d8a5651e0ddbdedaf25a7d3ae3f0b15f71ac73a1773" +dependencies = [ + "byteorder", + "bytes", + "rand 0.9.2", + "rtp", + "thiserror 1.0.61", +] + +[[package]] +name = "webrtc-sctp" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f985465467d8910c1f8ac4382cd64f83b1f6a1a75021a82b221546f6fb3b856f" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "crc", + "log", + "portable-atomic", + "rand 0.9.2", + "thiserror 1.0.61", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-srtp" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d8cdc33413f1d0192670a80ce93d17cb78d57fe3a2414be30d6f6dff121123" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "byteorder", + "bytes", + "ctr", + "hmac", + "log", + "rtcp", + "rtp", + "sha1", + "subtle", + "thiserror 1.0.61", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-util" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c0c7e0c8f280f2bbfae442701465777ac07adaf46ce0c5863cd58e13fe472a" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bytes", + "ipnet", + "lazy_static", + "log", + "nix 0.26.4", + "portable-atomic", + "rand 0.9.2", + "thiserror 1.0.61", + "tokio", + "winapi 0.3.9", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.34", +] + +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall 0.5.2", + "wasite", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "win-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7b128a98c1cfa201b09eb49ba285887deb3cbe7466a98850eb1adabb452be5" +dependencies = [ + "windows 0.34.0", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-wsapoll" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1eafc5f679c576995526e81635d0cf9695841736712b4e892f87abbe6fed3f28" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" +dependencies = [ + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + +[[package]] +name = "windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45296b64204227616fdbf2614cefa4c236b98ee64dfaaaa435207ed99fe7829f" +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", +] + +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-implement 0.52.0", + "windows-interface 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core 0.61.0", + "windows-future", + "windows-link 0.1.1", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.0", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link 0.1.1", + "windows-result 0.3.2", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +dependencies = [ + "windows-core 0.61.0", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-implement" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "windows-interface" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.0", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link 0.1.1", +] + +[[package]] +name = "windows-service" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9db37ecb5b13762d95468a2fc6009d4b2c62801243223aabd44fca13ad13c8" +dependencies = [ + "bitflags 1.3.2", + "widestring", + "windows-sys 0.45.0", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link 0.1.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-version" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-win" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e23e33622b3b52f948049acbec9bcc34bf6e26d74176b88941f213c75cf2dc" +dependencies = [ + "error-code", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0" +dependencies = [ + "ahash 0.8.12", + "android-activity", + "atomic-waker", + "bitflags 2.9.1", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk 0.9.0", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle 0.6.2", + "redox_syscall 0.4.1", + "rustix 0.38.34", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb 0.13.1", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winreg" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189" +dependencies = [ + "cfg-if 1.0.0", + "winapi 0.3.9", +] + +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de22eebb1d1e2bad2d970086e96da0e12cde0b411321e5b0f7b2a1f876aa26f" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 0.38.34", + "tempfile", + "thiserror 1.0.61", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "wol-rs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5a8a033ef9b208ec8b5946761958ed2b2693ac49b04f647fdc013000870b8f" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.19.0" +source = "git+https://github.com/bjornsnoen/x11-rs#c2e9bfaa7b196938f8700245564d8ac5d447786a" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-clipboard" +version = "0.8.1" +source = "git+https://github.com/clslaid/x11-clipboard?branch=feat/store-batch#5fc2e73bc01ada3681159b34cf3ea8f0d14cd904" +dependencies = [ + "x11rb 0.12.0", +] + +[[package]] +name = "x11-clipboard" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +dependencies = [ + "libc", + "x11rb 0.13.1", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" +dependencies = [ + "gethostname 0.3.0", + "nix 0.26.4", + "winapi 0.3.9", + "winapi-wsapoll", + "x11rb-protocol 0.12.0", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname 0.4.3", + "libc", + "libloading 0.8.4", + "once_cell", + "rustix 0.38.34", + "x11rb-protocol 0.13.1", +] + +[[package]] +name = "x11rb-protocol" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" +dependencies = [ + "nix 0.26.4", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde 1.0.228", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 1.0.61", + "time 0.3.36", +] + +[[package]] +name = "xattr" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" +dependencies = [ + "libc", + "linux-raw-sys 0.4.14", + "rustix 0.38.34", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xdg-home" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca91dcf8f93db085f3a0a29358cd0b9d670915468f4290e8b85d118a34211ab8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.9.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time 0.3.36", +] + +[[package]] +name = "zbus" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "byteorder", + "derivative", + "enumflags2", + "event-listener 2.5.3", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.26.4", + "once_cell", + "ordered-stream", + "rand 0.8.5", + "serde 1.0.228", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi 0.3.9", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" +dependencies = [ + "serde 1.0.228", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.34", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive 0.8.26", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq 0.1.5", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time 0.3.36", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe 7.1.0", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.11+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zvariant" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde 1.0.228", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] diff --git a/shelled/rustdesk-as-ref/Cargo.toml b/shelled/rustdesk-as-ref/Cargo.toml new file mode 100644 index 0000000..3961e9d --- /dev/null +++ b/shelled/rustdesk-as-ref/Cargo.toml @@ -0,0 +1,247 @@ +[package] +name = "rustdesk" +version = "1.4.6" +authors = ["rustdesk "] +edition = "2021" +build= "build.rs" +description = "RustDesk Remote Desktop" +default-run = "rustdesk" +rust-version = "1.75" + +[lib] +name = "librustdesk" +crate-type = ["cdylib", "staticlib", "rlib"] + +[[bin]] +name = "naming" +path = "src/naming.rs" + +[[bin]] +name = "service" +path = "src/service.rs" + +[features] +inline = [] +cli = [] +use_samplerate = ["samplerate"] +use_rubato = ["rubato"] +use_dasp = ["dasp"] +flutter = ["flutter_rust_bridge"] +default = ["use_dasp"] +hwcodec = ["scrap/hwcodec"] +vram = ["scrap/vram"] +mediacodec = ["scrap/mediacodec"] +plugin_framework = [] +linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"] +unix-file-copy-paste = [ + "dep:x11-clipboard", + "dep:x11rb", + "dep:percent-encoding", + "dep:once_cell", + "clipboard/unix-file-copy-paste", +] +screencapturekit = ["cpal/screencapturekit"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = "0.1" +scrap = { path = "libs/scrap", features = ["wayland"] } +hbb_common = { path = "libs/hbb_common" } +serde_derive = "1.0" +serde = "1.0" +serde_json = "1.0" +serde_repr = "0.1" +cfg-if = "1.0" +lazy_static = "1.4" +sha2 = "0.10" +repng = "0.2" +parity-tokio-ipc = { git = "https://github.com/rustdesk-org/parity-tokio-ipc" } +magnum-opus = { git = "https://github.com/rustdesk-org/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 } +uuid = { version = "1.3", features = ["v4"] } +clap = "4.2" +rpassword = "7.2" +num_cpus = "1.15" +bytes = { version = "1.4", features = ["serde"] } +default-net = "0.14" +wol-rs = "1.0" +flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true} +errno = "0.3" +rdev = { git = "https://github.com/rustdesk-org/rdev" } +url = { version = "2.3", features = ["serde"] } +crossbeam-queue = "0.3" +hex = "0.4" +chrono = "0.4" +cidr-utils = "0.5" +fon = "0.6" +zip = "0.6" +shutdown_hooks = "0.1" +totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } +stunclient = "0.4" +kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"} +reqwest = { version = "0.12", features = ["blocking", "socks", "json", "native-tls", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false } + +[target.'cfg(not(target_os = "linux"))'.dependencies] +# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux +cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" } +ringbuf = "0.3" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +mac_address = "1.1" +sciter-rs = { git = "https://github.com/rustdesk-org/rust-sciter", branch = "dyn" } +sys-locale = "0.3" +enigo = { path = "libs/enigo", features = [ "with_serde" ] } +clipboard = { path = "libs/clipboard" } +ctrlc = "3.2" +# arboard = { version = "3.4", features = ["wayland-data-control"] } +arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } +clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } +portable-pty = { git = "https://github.com/rustdesk-org/wezterm", branch = "rustdesk/pty_based_0.8.1", package = "portable-pty" } + +system_shutdown = "4.0" +qrcode-generator = "4.1" + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3", features = [ + "winuser", + "wincrypt", + "shellscalingapi", + "pdh", + "synchapi", + "memoryapi", + "shellapi", + "devguid", + "setupapi", + "cguid", + "cfgmgr32", + "ioapiset", + "winspool", +] } +windows = { version = "0.61", features = [ + "Win32", + "Win32_Foundation", + "Win32_Security", + "Win32_Security_Authorization", + "Win32_Storage_FileSystem", + "Win32_System", + "Win32_System_Diagnostics", + "Win32_System_Diagnostics_ToolHelp", + "Win32_System_Environment", + "Win32_System_IO", + "Win32_System_Memory", + "Win32_System_Pipes", + "Win32_System_Threading", + "Win32_UI_Shell", +] } +winreg = "0.11" +windows-service = "0.6" +virtual_display = { path = "libs/virtual_display" } +remote_printer = { path = "libs/remote_printer" } +impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" } +shared_memory = "0.12" +tauri-winrt-notification = "0.1" +runas = "1.2" + +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2" +cocoa = "0.24" +dispatch = "0.2" +core-foundation = "0.9" +core-graphics = "0.22" +include_dir = "0.7" +fruitbasket = "0.10" +objc_id = "0.1" +# If we use piet "0.7" here, we must also update core-graphics to "0.24". +piet = "0.6" +piet-coregraphics = "0.6" +foreign-types = "0.3" + +[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] +tray-icon = { git = "https://github.com/tauri-apps/tray-icon", version = "0.21.3" } +tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" } +image = "0.24" + +[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] +keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" } + +[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] +wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" } +tiny-skia = "0.11" +softbuffer = "0.4" +fontdb = "0.23" +bytemuck = "1.23" +ttf-parser = "0.25" + +[target.'cfg(target_os = "linux")'.dependencies] +libxdo-sys = "0.11" +psimple = { package = "libpulse-simple-binding", version = "2.27" } +pulse = { package = "libpulse-binding", version = "2.27" } +rust-pulsectl = { git = "https://github.com/rustdesk-org/pulsectl" } +async-process = "1.7" +evdev = { git="https://github.com/rustdesk-org/evdev" } +dbus = "0.9" +dbus-crossroads = "0.5" +pam = { git="https://github.com/rustdesk-org/pam" } +x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} +x11rb = {version = "0.12", features = ["all-extensions"], optional = true} +percent-encoding = {version = "2.3", optional = true} +once_cell = {version = "1.18", optional = true} +nix = { version = "0.29", features = ["term", "process"]} +gtk = "0.18" +termios = "0.3" +terminfo = "0.8" +winit = "0.30" + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +openssl = { version = "0.10", features = ["vendored"] } + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.13" +jni = "0.21" +android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" } + +[workspace] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"] +exclude = ["vdi/host", "examples/custom_plugin"] + +# Patch libxdo-sys to use a stub implementation that doesn't require libxdo +# This allows building and running on systems without libxdo installed (e.g., Wayland-only) +[patch.crates-io] +libxdo-sys = { path = "libs/libxdo-sys-stub" } + +[package.metadata.winres] +LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved." +ProductName = "RustDesk" +FileDescription = "RustDesk Remote Desktop" +OriginalFilename = "rustdesk.exe" + +[target.'cfg(target_os="windows")'.build-dependencies] +winres = "0.1" +winapi = { version = "0.3", features = [ "winnt", "pdh", "synchapi" ] } + +[build-dependencies] +cc = "1.0" +hbb_common = { path = "libs/hbb_common" } +os-version = "0.2" + +[dev-dependencies] +hound = "3.5" +docopt = "1.1" + +[package.metadata.bundle] +name = "RustDesk" +identifier = "com.carriez.rustdesk" +icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] +osx_minimum_system_version = "10.14" + +#https://github.com/johnthagen/min-sized-rust +[profile.release] +lto = true +codegen-units = 1 +panic = 'abort' +strip = true +#opt-level = 'z' # only have smaller size after strip +rpath = true diff --git a/shelled/rustdesk-as-ref/Dockerfile b/shelled/rustdesk-as-ref/Dockerfile new file mode 100644 index 0000000..f0e4e4a --- /dev/null +++ b/shelled/rustdesk-as-ref/Dockerfile @@ -0,0 +1,64 @@ +FROM debian:bullseye-slim + +WORKDIR / +ARG DEBIAN_FRONTEND=noninteractive +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 +RUN apt update -y && \ + apt install --yes --no-install-recommends \ + g++ \ + gcc \ + git \ + curl \ + nasm \ + yasm \ + libgtk-3-dev \ + clang \ + libxcb-randr0-dev \ + libxdo-dev \ + libxfixes-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libasound2-dev \ + libpam0g-dev \ + libpulse-dev \ + make \ + wget \ + libssl-dev \ + unzip \ + zip \ + sudo \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + ca-certificates \ + ninja-build && \ + rm -rf /var/lib/apt/lists/* + +RUN wget https://github.com/Kitware/CMake/releases/download/v3.30.6/cmake-3.30.6.tar.gz --no-check-certificate && \ + tar xzf cmake-3.30.6.tar.gz && \ + cd cmake-3.30.6 && \ + ./configure --prefix=/usr/local && \ + make && \ + make install + +RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \ + /vcpkg/bootstrap-vcpkg.sh -disableMetrics && \ + /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom + +RUN groupadd -r user && \ + useradd -r -g user user --home /home/user && \ + mkdir -p /home/user/rustdesk && \ + chown -R user: /home/user && \ + echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user + +WORKDIR /home/user +RUN curl -LO https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so + +USER user +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh && \ + chmod +x rustup.sh && \ + ./rustup.sh -y + +USER root +ENV HOME=/home/user +COPY ./entrypoint.sh / +ENTRYPOINT ["/entrypoint.sh"] diff --git a/shelled/rustdesk-as-ref/LICENCE b/shelled/rustdesk-as-ref/LICENCE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/shelled/rustdesk-as-ref/LICENCE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/shelled/rustdesk-as-ref/README.md b/shelled/rustdesk-as-ref/README.md new file mode 100644 index 0000000..ae5c8d3 --- /dev/null +++ b/shelled/rustdesk-as-ref/README.md @@ -0,0 +1,182 @@ +

+ RustDesk - Your remote desktop
+ Build • + Docker • + Structure • + Snapshot
+ [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk] | [Română]
+ We need your help to translate this README, RustDesk UI and RustDesk Doc to your native language +

+ +> [!Caution] +> **Misuse Disclaimer:**
+> The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application. + + +Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Advanced%20Features-blue)](https://rustdesk.com/pricing.html) + +Yet another remote desktop solution, written in Rust. Works out of the box with no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for help getting started. + +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) + +## Dependencies + +Desktop versions use Flutter or Sciter (deprecated) for GUI, this tutorial is for Sciter only, since it is easier and more friendly to start. Check out our [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for building Flutter version. + +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) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Raw Steps to build + +- Prepare your Rust development env and C++ build env + +- Install [vcpkg](https://github.com/microsoft/vcpkg), and set `VCPKG_ROOT` env variable correctly + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/macOS: vcpkg install libvpx libyuv opus aom + +- run `cargo run` + +## [Build](https://rustdesk.com/docs/en/dev/build/) + +## How to Build on Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Install vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Fix libvpx (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Build + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone --recurse-submodules https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## How to build with Docker + +Begin by cloning the repository and building the Docker container: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +git submodule update --init --recursive +docker build -t "rustdesk-builder" . +``` + +Then, each time you need to build the application, run the following command: + +```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 +``` + +Note that the first build may take longer before dependencies are cached, subsequent builds will be faster. Additionally, if you need to specify different arguments to the build command, you may do so at the end of the command in the `` position. For instance, if you wanted to build an optimized release version, you would run the command above followed by `--release`. The resulting executable will be available in the target folder on your system, and can be run with: + +```sh +target/debug/rustdesk +``` + +Or, if you're running a release executable: + +```sh +target/release/rustdesk +``` + +Please ensure that you run these commands from the root of the RustDesk repository, or the application may not find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host. + +## File Structure + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: file copy and paste implementation for Windows, Linux, macOS. +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: obsolete Sciter UI (deprecated) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript for Flutter web client + +## Screenshots + +![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) + diff --git a/shelled/rustdesk-as-ref/appimage/AppImageBuilder-aarch64.yml b/shelled/rustdesk-as-ref/appimage/AppImageBuilder-aarch64.yml new file mode 100644 index 0000000..64d6c2c --- /dev/null +++ b/shelled/rustdesk-as-ref/appimage/AppImageBuilder-aarch64.yml @@ -0,0 +1,102 @@ +# appimage-builder recipe see https://appimage-builder.readthedocs.io for details +version: 1 +script: + - rm -rf ./AppDir || true + - bsdtar -zxvf rustdesk.deb + - tar -xvf ./data.tar.xz + - mkdir ./AppDir + - mv ./usr ./AppDir/usr + # 32x32 icon + - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done + - mkdir -p ./AppDir/usr/share/icons/hicolor/scalable/apps/; cp ../res/scalable.svg ./AppDir/usr/share/icons/hicolor/scalable/apps/rustdesk.svg + # desktop file + # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop + - rm -rf ./AppDir/usr/share/applications +AppDir: + path: ./AppDir + app_info: + id: rustdesk + name: rustdesk + icon: rustdesk + version: 1.4.6 + exec: usr/share/rustdesk/rustdesk + exec_args: $@ + apt: + arch: + - arm64 + allow_unauthenticated: true + sources: + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted + universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted + universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + include: + - libc6:arm64 + - libgtk-3-0 + - libxcb-randr0 + - libxdo3 + - libxfixes3 + - libxcb-shape0 + - libxcb-xfixes0 + - libasound2 + - libsystemd0 + - curl + - libva2 + - libva-drm2 + - libva-x11-2 + - libgstreamer-plugins-base1.0-0 + - gstreamer1.0-pipewire + - libwayland-client0 + - libwayland-cursor0 + - libwayland-egl1 + - libpulse0 + - packagekit-gtk3-module + - libcanberra-gtk3-module + - libpam0g + - libdrm2 + exclude: + - humanity-icon-theme + - hicolor-icon-theme + - adwaita-icon-theme + - ubuntu-mono + files: + include: [] + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + runtime: + env: + GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules + GDK_BACKEND: x11 + APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64 + GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0 + GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0 + test: + fedora-30: + image: appimagecrafters/tests-env:fedora-30 + command: ./AppRun + debian-stable: + image: appimagecrafters/tests-env:debian-stable + command: ./AppRun + archlinux-latest: + image: appimagecrafters/tests-env:archlinux-latest + command: ./AppRun + centos-7: + image: appimagecrafters/tests-env:centos-7 + command: ./AppRun + ubuntu-xenial: + image: appimagecrafters/tests-env:ubuntu-xenial + command: ./AppRun +AppImage: + arch: aarch64 + update-information: guess + comp: gzip diff --git a/shelled/rustdesk-as-ref/appimage/AppImageBuilder-x86_64.yml b/shelled/rustdesk-as-ref/appimage/AppImageBuilder-x86_64.yml new file mode 100644 index 0000000..933673c --- /dev/null +++ b/shelled/rustdesk-as-ref/appimage/AppImageBuilder-x86_64.yml @@ -0,0 +1,105 @@ +# appimage-builder recipe see https://appimage-builder.readthedocs.io for details +version: 1 +script: + - rm -rf ./AppDir || true + - bsdtar -zxvf rustdesk.deb + - tar -xvf ./data.tar.xz + - mkdir ./AppDir + - mv ./usr ./AppDir/usr + # 32x32 icon + - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done + - mkdir -p ./AppDir/usr/share/icons/hicolor/scalable/apps/; cp ../res/scalable.svg ./AppDir/usr/share/icons/hicolor/scalable/apps/rustdesk.svg + # desktop file + # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop + - rm -rf ./AppDir/usr/share/applications +AppDir: + path: ./AppDir + app_info: + id: rustdesk + name: rustdesk + icon: rustdesk + version: 1.4.6 + exec: usr/share/rustdesk/rustdesk + exec_args: $@ + apt: + arch: + - amd64 + allow_unauthenticated: true + sources: + - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted + universe multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted + universe multiverse + include: + # https://github.com/rustdesk/rustdesk/issues/9103 + # Because of APPDIR_LIBRARY_PATH, this libc6 is not used, use LD_PRELOAD: $APPDIR/usr/lib/x86_64-linux-gnu/libc.so.6 may help, If you have time, please have a try. + # We modify APPDIR_LIBRARY_PATH to use system lib first because gst crashed if not doing so, but you can try to change it. + - libc6:amd64 + - libgtk-3-0 + - libxcb-randr0 + - libxdo3 + - libxfixes3 + - libxcb-shape0 + - libxcb-xfixes0 + - libasound2 + - libsystemd0 + - curl + - libva2 + - libva-drm2 + - libva-x11-2 + - libgstreamer-plugins-base1.0-0 + - gstreamer1.0-pipewire + - libwayland-client0 + - libwayland-cursor0 + - libwayland-egl1 + - libpulse0 + - packagekit-gtk3-module + - libcanberra-gtk3-module + - libpam0g + - libdrm2 + exclude: + - humanity-icon-theme + - hicolor-icon-theme + - adwaita-icon-theme + - ubuntu-mono + files: + include: [] + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + runtime: + env: + GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules + GDK_BACKEND: x11 + APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64 + GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0 + GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0 + test: + fedora-30: + image: appimagecrafters/tests-env:fedora-30 + command: ./AppRun + debian-stable: + image: appimagecrafters/tests-env:debian-stable + command: ./AppRun + archlinux-latest: + image: appimagecrafters/tests-env:archlinux-latest + command: ./AppRun + centos-7: + image: appimagecrafters/tests-env:centos-7 + command: ./AppRun + ubuntu-xenial: + image: appimagecrafters/tests-env:ubuntu-xenial + command: ./AppRun +AppImage: + arch: x86_64 + update-information: guess + comp: gzip diff --git a/shelled/rustdesk-as-ref/build.py b/shelled/rustdesk-as-ref/build.py new file mode 100644 index 0000000..ce9a09e --- /dev/null +++ b/shelled/rustdesk-as-ref/build.py @@ -0,0 +1,647 @@ +#!/usr/bin/env python3 + +import os +import pathlib +import platform +import zipfile +import urllib.request +import shutil +import hashlib +import argparse +import sys +from pathlib import Path + +windows = platform.platform().startswith('Windows') +osx = platform.platform().startswith( + 'Darwin') or platform.platform().startswith("macOS") +hbb_name = 'rustdesk' + ('.exe' if windows else '') +exe_path = 'target/release/' + hbb_name +if windows: + flutter_build_dir = 'build/windows/x64/runner/Release/' +elif osx: + flutter_build_dir = 'build/macos/Build/Products/Release/' +else: + flutter_build_dir = 'build/linux/x64/release/bundle/' +flutter_build_dir_2 = f'flutter/{flutter_build_dir}' +skip_cargo = False + + +def get_deb_arch() -> str: + custom_arch = os.environ.get("DEB_ARCH") + if custom_arch is None: + return "amd64" + return custom_arch + +def get_deb_extra_depends() -> str: + custom_arch = os.environ.get("DEB_ARCH") + if custom_arch == "armhf": # for arm32v7 libsciter-gtk.so + return ", libatomic1" + return "" + +def system2(cmd): + exit_code = os.system(cmd) + if exit_code != 0: + sys.stderr.write(f"Error occurred when executing: `{cmd}`. Exiting.\n") + sys.exit(-1) + + +def get_version(): + with open("Cargo.toml", encoding="utf-8") as fh: + for line in fh: + if line.startswith("version"): + return line.replace("version", "").replace("=", "").replace('"', '').strip() + return '' + + +def parse_rc_features(feature): + available_features = {} + apply_features = {} + if not feature: + feature = [] + + def platform_check(platforms): + if windows: + return 'windows' in platforms + elif osx: + return 'osx' in platforms + else: + return 'linux' in platforms + + def get_all_features(): + features = [] + for (feat, feat_info) in available_features.items(): + if platform_check(feat_info['platform']): + features.append(feat) + return features + + if isinstance(feature, str) and feature.upper() == 'ALL': + return get_all_features() + elif isinstance(feature, list): + if windows: + # download third party is deprecated, we use github ci instead. + # feature.append('PrivacyMode') + pass + for feat in feature: + if isinstance(feat, str) and feat.upper() == 'ALL': + return get_all_features() + if feat in available_features: + if platform_check(available_features[feat]['platform']): + apply_features[feat] = available_features[feat] + else: + print(f'Unrecognized feature {feat}') + return apply_features + else: + raise Exception(f'Unsupported features param {feature}') + + +def make_parser(): + parser = argparse.ArgumentParser(description='Build script.') + parser.add_argument( + '-f', + '--feature', + dest='feature', + metavar='N', + type=str, + nargs='+', + default='', + help='Integrate features, windows only.' + 'Available: [Not used for now]. 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', + help='Enable feature hwcodec' + ( + '' if windows or osx else ', need libva-dev.') + ) + parser.add_argument( + '--vram', + action='store_true', + help='Enable feature vram, only available on windows now.' + ) + parser.add_argument( + '--portable', + action='store_true', + help='Build windows portable' + ) + parser.add_argument( + '--unix-file-copy-paste', + action='store_true', + help='Build with unix file copy paste feature' + ) + parser.add_argument( + '--skip-cargo', + action='store_true', + help='Skip cargo build process, only flutter version + Linux supported currently' + ) + if windows: + parser.add_argument( + '--skip-portable-pack', + action='store_true', + help='Skip packing, only flutter version + Windows supported' + ) + parser.add_argument( + "--package", + type=str + ) + if osx: + parser.add_argument( + '--screencapturekit', + action='store_true', + help='Enable feature screencapturekit' + ) + return parser + + +# Generate build script for docker +# +# it assumes all build dependencies are installed in environments +# Note: do not use it in bare metal, or may break build environments +def generate_build_script_for_docker(): + with open("/tmp/build.sh", "w") as f: + f.write(''' + #!/bin/bash + # environment + export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation: " | cut -d' ' -f4-)/include" + # flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.0.5-stable.tar.xz + tar -xvf flutter_linux_3.0.5-stable.tar.xz + export PATH=`pwd`/flutter/bin:$PATH + popd + # flutter_rust_bridge + dart pub global activate ffigen --version 5.0.1 + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + # install vcpkg + pushd /opt + export VCPKG_ROOT=`pwd`/vcpkg + git clone https://github.com/microsoft/vcpkg + vcpkg/bootstrap-vcpkg.sh + popd + $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed" + # build rustdesk + ./build.py --flutter --hwcodec + ''') + system2("chmod +x /tmp/build.sh") + system2("bash /tmp/build.sh") + + +# Downloading third party resources is deprecated. +# We can use this function in an offline build environment. +# Even in an online environment, we recommend building third-party resources yourself. +def download_extract_features(features, res_dir): + import re + + proxy = '' + + def req(url): + if not proxy: + return url + else: + r = urllib.request.Request(url) + r.set_proxy(proxy, 'http') + r.set_proxy(proxy, 'https') + return r + + for (feat, feat_info) in features.items(): + includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else [] + includes = [re.compile(p) for p in includes] + excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else [] + excludes = [re.compile(p) for p in excludes] + + print(f'{feat} download begin') + download_filename = feat_info['zip_url'].split('/')[-1] + checksum_md5_response = urllib.request.urlopen( + req(feat_info['checksum_url'])) + for line in checksum_md5_response.read().decode('utf-8').splitlines(): + if line.split()[1] == download_filename: + checksum_md5 = line.split()[0] + filename, _headers = urllib.request.urlretrieve(feat_info['zip_url'], + download_filename) + md5 = hashlib.md5(open(filename, 'rb').read()).hexdigest() + if checksum_md5 != md5: + raise Exception(f'{feat} download failed') + print(f'{feat} download end. extract bein') + zip_file = zipfile.ZipFile(filename) + zip_list = zip_file.namelist() + for f in zip_list: + file_exclude = False + for p in excludes: + if p.match(f) is not None: + file_exclude = True + break + if file_exclude: + continue + + file_include = False if includes else True + for p in includes: + if p.match(f) is not None: + file_include = True + break + if file_include: + print(f'extract file {f}') + zip_file.extract(f, res_dir) + zip_file.close() + os.remove(download_filename) + print(f'{feat} extract end') + + +def external_resources(flutter, args, res_dir): + features = parse_rc_features(args.feature) + if not features: + return + + print(f'Build with features {list(features.keys())}') + if os.path.isdir(res_dir) and not os.path.islink(res_dir): + shutil.rmtree(res_dir) + elif os.path.exists(res_dir): + raise Exception(f'Find file {res_dir}, not a directory') + os.makedirs(res_dir, exist_ok=True) + download_extract_features(features, res_dir) + if flutter: + os.makedirs(flutter_build_dir_2, exist_ok=True) + for f in pathlib.Path(res_dir).iterdir(): + print(f'{f}') + if f.is_file(): + shutil.copy2(f, flutter_build_dir_2) + else: + shutil.copytree(f, f'{flutter_build_dir_2}{f.stem}') + + +def get_features(args): + features = ['inline'] if not args.flutter else [] + if args.hwcodec: + features.append('hwcodec') + if args.vram: + features.append('vram') + if args.flutter: + features.append('flutter') + if args.unix_file_copy_paste: + features.append('unix-file-copy-paste') + if osx: + if args.screencapturekit: + features.append('screencapturekit') + print("features:", features) + return features + + +def generate_control_file(version): + control_file_path = "../res/DEBIAN/control" + system2('/bin/rm -rf %s' % control_file_path) + + content = """Package: rustdesk +Section: net +Priority: optional +Version: %s +Architecture: %s +Maintainer: rustdesk +Homepage: https://rustdesk.com +Depends: libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s +Recommends: libayatana-appindicator3-1 +Description: A remote control software. + +""" % (version, get_deb_arch(), get_deb_extra_depends()) + file = open(control_file_path, "w") + file.write(content) + file.close() + + +def ffi_bindgen_function_refactor(): + # workaround ffigen + system2( + 'sed -i "s/ffi.NativeFunction> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") + + system2('mkdir -p tmpdeb/DEBIAN') + generate_control_file(version) + system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') + md5_file_folder("tmpdeb/") + system2('dpkg-deb -b tmpdeb rustdesk.deb;') + + system2('/bin/rm -rf tmpdeb/') + system2('/bin/rm -rf ../res/DEBIAN/control') + os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) + os.chdir("..") + + +def build_deb_from_folder(version, binary_folder): + os.chdir('flutter') + system2('mkdir -p tmpdeb/usr/bin/') + system2('mkdir -p tmpdeb/usr/share/rustdesk') + system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/') + system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/') + system2('mkdir -p tmpdeb/usr/share/applications/') + system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') + system2('rm tmpdeb/usr/bin/rustdesk || true') + system2( + f'cp -r ../{binary_folder}/* tmpdeb/usr/share/rustdesk/') + system2( + 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + system2( + 'cp ../res/128x128@2x.png tmpdeb/usr/share/icons/hicolor/256x256/apps/rustdesk.png') + system2( + 'cp ../res/scalable.svg tmpdeb/usr/share/icons/hicolor/scalable/apps/rustdesk.svg') + system2( + 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + system2( + 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') + system2( + "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") + + system2('mkdir -p tmpdeb/DEBIAN') + generate_control_file(version) + system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') + md5_file_folder("tmpdeb/") + system2('dpkg-deb -b tmpdeb rustdesk.deb;') + + system2('/bin/rm -rf tmpdeb/') + system2('/bin/rm -rf ../res/DEBIAN/control') + os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) + os.chdir("..") + + +def build_flutter_dmg(version, features): + if not skip_cargo: + # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project + system2( + f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release') + # copy dylib + system2( + "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") + os.chdir('flutter') + system2('flutter build macos --release') + system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/') + ''' + system2( + "create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") + os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") + ''' + os.chdir("..") + + +def build_flutter_arch_manjaro(version, features): + if not skip_cargo: + system2(f'cargo build --features {features} --lib --release') + ffi_bindgen_function_refactor() + os.chdir('flutter') + system2('flutter build linux --release') + system2(f'strip {flutter_build_dir}/lib/librustdesk.so') + os.chdir('../res') + system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f') + + +def build_flutter_windows(version, features, skip_portable_pack): + if not skip_cargo: + system2(f'cargo build --features {features} --lib --release') + if not os.path.exists("target/release/librustdesk.dll"): + print("cargo build failed, please check rust source code.") + exit(-1) + os.chdir('flutter') + system2('flutter build windows --release') + os.chdir('..') + shutil.copy2('target/release/deps/dylib_virtual_display.dll', + flutter_build_dir_2) + if skip_portable_pack: + return + os.chdir('libs/portable') + system2('pip3 install -r requirements.txt') + system2( + f'python3 ./generate.py -f ../../{flutter_build_dir_2} -o . -e ../../{flutter_build_dir_2}/rustdesk.exe') + os.chdir('../..') + if os.path.exists('./rustdesk_portable.exe'): + os.replace('./target/release/rustdesk-portable-packer.exe', + './rustdesk_portable.exe') + else: + os.rename('./target/release/rustdesk-portable-packer.exe', + './rustdesk_portable.exe') + print( + f'output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe') + os.rename('./rustdesk_portable.exe', f'./rustdesk-{version}-install.exe') + print( + f'output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe') + + +def main(): + global skip_cargo + parser = make_parser() + args = parser.parse_args() + + if os.path.exists(exe_path): + os.unlink(exe_path) + if os.path.isfile('/usr/bin/pacman'): + system2('git checkout src/ui/common.tis') + version = get_version() + features = ','.join(get_features(args)) + flutter = args.flutter + if not flutter: + system2('python3 res/inline-sciter.py') + print(args.skip_cargo) + if args.skip_cargo: + skip_cargo = True + portable = args.portable + package = args.package + if package: + build_deb_from_folder(version, package) + return + res_dir = 'resources' + external_resources(flutter, args, res_dir) + if windows: + # build virtual display dynamic library + os.chdir('libs/virtual_display/dylib') + system2('cargo build --release') + os.chdir('../../..') + + if flutter: + build_flutter_windows(version, features, args.skip_portable_pack) + return + system2('cargo build --release --features ' + features) + # system2('upx.exe target/release/rustdesk.exe') + system2('mv target/release/rustdesk.exe target/release/RustDesk.exe') + pa = os.environ.get('P') + if pa: + # https://certera.com/kb/tutorial-guide-for-safenet-authentication-client-for-code-signing/ + system2( + f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com ' + 'target\\release\\rustdesk.exe') + else: + print('Not signed') + system2( + f'cp -rf target/release/RustDesk.exe {res_dir}') + os.chdir('libs/portable') + system2('pip3 install -r requirements.txt') + system2( + f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe') + system2('mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..') + elif os.path.isfile('/usr/bin/pacman'): + # pacman -S -needed base-devel + system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) + if flutter: + build_flutter_arch_manjaro(version, features) + else: + system2('cargo build --release --features ' + features) + system2('git checkout src/ui/common.tis') + system2('strip target/release/rustdesk') + system2('ln -s res/pacman_install && ln -s res/PKGBUILD') + system2('HBB=`pwd` makepkg -f') + system2('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'): + system2('cargo build --release --features ' + features) + system2('strip target/release/rustdesk') + system2( + "sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version) + system2('HBB=`pwd` rpmbuild -ba res/rpm.spec') + system2( + 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % ( + version, version)) + # yum localinstall rustdesk.rpm + elif os.path.isfile('/usr/bin/zypper'): + system2('cargo build --release --features ' + features) + system2('strip target/release/rustdesk') + system2( + "sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version) + system2('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') + system2( + 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % ( + version, version)) + # yum localinstall rustdesk.rpm + else: + if flutter: + if osx: + build_flutter_dmg(version, features) + pass + else: + # system2( + # 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') + build_flutter_deb(version, features) + else: + system2('cargo bundle --release --features ' + features) + if osx: + system2( + 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') + system2( + 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') + # https://github.com/sindresorhus/create-dmg + system2('/bin/rm -rf *.dmg') + pa = os.environ.get('P') + if pa: + system2(''' + # buggy: rcodesign sign ... path/*, have to sign one by one + # install rcodesign via cargo install apple-codesign + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/libsciter.dylib + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app + # 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)) + system2( + 'create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version) + os.rename('RustDesk %s.dmg' % + version, 'rustdesk-%s.dmg' % version) + if pa: + system2(''' + # https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html + # https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html + # https://developer.apple.com/developer-id/ + # goto xcode and login with apple id, manager certificates (Developer ID Application and/or Developer ID Installer) online there (only download and double click (install) cer file can not export p12 because no private key) + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg + codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg + # https://appstoreconnect.apple.com/access/api + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_getting_started.html#apple-codesign-app-store-connect-api-key + # p8 file is generated when you generate api key (can download only once) + rcodesign notary-submit --api-key-path ../.p12/api-key.json --staple rustdesk-{1}.dmg + # verify: spctl -a -t exec -v /Applications/RustDesk.app + '''.format(pa, version)) + else: + print('Not signed') + else: + # build deb package + system2( + 'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') + system2('dpkg-deb -R rustdesk.deb tmpdeb') + system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/') + system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/') + system2( + 'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + system2( + 'cp res/128x128@2x.png tmpdeb/usr/share/icons/hicolor/256x256/apps/rustdesk.png') + system2( + 'cp res/scalable.svg tmpdeb/usr/share/icons/hicolor/scalable/apps/rustdesk.svg') + system2( + 'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + system2( + 'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') + os.system('mkdir -p tmpdeb/etc/rustdesk/') + os.system('cp -a res/startwm.sh tmpdeb/etc/rustdesk/') + os.system('mkdir -p tmpdeb/etc/X11/rustdesk/') + os.system('cp res/xorg.conf tmpdeb/etc/X11/rustdesk/') + os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/') + os.system('mkdir -p tmpdeb/etc/pam.d/') + os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk') + system2('strip tmpdeb/usr/bin/rustdesk') + system2('mkdir -p tmpdeb/usr/share/rustdesk') + system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/') + system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/') + md5_file_folder("tmpdeb/") + system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') + os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) + + +def md5_file(fn): + md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest() + system2('echo "%s /%s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) + +def md5_file_folder(base_dir): + base_path = Path(base_dir) + for file in base_path.rglob('*'): + if file.is_file() and 'DEBIAN' not in file.parts: + relative_path = file.relative_to(base_path) + md5_file(str(relative_path)) + + +if __name__ == "__main__": + main() diff --git a/shelled/rustdesk-as-ref/build.rs b/shelled/rustdesk-as-ref/build.rs new file mode 100644 index 0000000..92fb1f4 --- /dev/null +++ b/shelled/rustdesk-as-ref/build.rs @@ -0,0 +1,94 @@ +#[cfg(windows)] +fn build_windows() { + let file = "src/platform/windows.cc"; + let file2 = "src/platform/windows_delete_test_cert.cc"; + cc::Build::new().file(file).file(file2).compile("windows"); + println!("cargo:rustc-link-lib=WtsApi32"); + println!("cargo:rerun-if-changed={}", file); + println!("cargo:rerun-if-changed={}", file2); +} + +#[cfg(target_os = "macos")] +fn build_mac() { + let file = "src/platform/macos.mm"; + let mut b = cc::Build::new(); + if let Ok(os_version::OsVersion::MacOS(v)) = os_version::detect() { + let v = v.version; + if v.contains("10.14") { + b.flag("-DNO_InputMonitoringAuthStatus=1"); + } + } + b.flag("-std=c++17").file(file).compile("macos"); + println!("cargo:rerun-if-changed={}", file); +} + +#[cfg(all(windows, feature = "inline"))] +fn build_manifest() { + use std::io::Write; + if std::env::var("PROFILE").unwrap() == "release" { + let mut res = winres::WindowsResource::new(); + res.set_icon("res/icon.ico") + .set_language(winapi::um::winnt::MAKELANGID( + winapi::um::winnt::LANG_ENGLISH, + winapi::um::winnt::SUBLANG_ENGLISH_US, + )) + .set_manifest_file("res/manifest.xml"); + match res.compile() { + Err(e) => { + write!(std::io::stderr(), "{}", e).unwrap(); + std::process::exit(1); + } + Ok(_) => {} + } + } +} + +fn install_android_deps() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + if target_os != "android" { + return; + } + let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_arch == "x86_64" { + target_arch = "x64".to_owned(); + } else if target_arch == "x86" { + target_arch = "x86".to_owned(); + } else if target_arch == "aarch64" { + target_arch = "arm64".to_owned(); + } else { + target_arch = "arm".to_owned(); + } + let target = format!("{}-android", target_arch); + let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); + let mut path: std::path::PathBuf = vcpkg_root.into(); + if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") { + path = vcpkg_root.into(); + } else { + path.push("installed"); + } + path.push(target); + println!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ); + println!("cargo:rustc-link-lib=ndk_compat"); + println!("cargo:rustc-link-lib=oboe"); + println!("cargo:rustc-link-lib=c++"); + println!("cargo:rustc-link-lib=OpenSLES"); +} + +fn main() { + hbb_common::gen_version(); + install_android_deps(); + #[cfg(all(windows, feature = "inline"))] + build_manifest(); + #[cfg(windows)] + build_windows(); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + if target_os == "macos" { + #[cfg(target_os = "macos")] + build_mac(); + println!("cargo:rustc-link-lib=framework=ApplicationServices"); + } + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-DE.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-DE.md new file mode 100644 index 0000000..ea42545 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-DE.md @@ -0,0 +1,137 @@ + +# Verhaltenskodex (Code of Conduct) für Mitwirkende + +## Unsere Verpflichtung + +Wir als Mitglieder, Mitwirkende und Führungskräfte verpflichten uns, +die Teilnahme unserer Community zu einer Erfahrung zu machen, +die für alle frei von Belästigungen ist, unabhängig von Alter, Körpergröße, +sichtbarer oder unsichtbarer Behinderung, ethnischer Zugehörigkeit, +Geschlechtsmerkmalen, Geschlechtsidentität und -ausdruck, Erfahrungsniveau, +Bildung, sozioökonomischem Status, Nationalität, persönlichem Erscheinungsbild, +Rasse, Religion oder sexueller Identität und Orientierung. + +Wir verpflichten uns, so zu handeln und zu interagieren, dass wir zu einer offenen, +einladenden, vielfältigen, integrativen und lebendigen Gemeinschaft beitragen. + +## Unsere Standards + +Beispiele für Verhaltensweisen, die zu einem positiven Umfeld für unsere +Gemeinschaft beitragen, sind: + +* Empathie und Freundlichkeit gegenüber anderen Menschen zu zeigen +* Respektvoll gegenüber anderen Meinungen, Sichtweisen und Erfahrungen zu sein +* Das Vergeben von sowie das großzügige Empfangen von konstruktivem Feedback +* Verantwortung übernehmen, sich bei den Betroffenen entschuldigen + und aus den Erfahrungen lernen +* Nicht darauf zu achten, was das Beste für sich selbst, + sondern zu Achten, was das Beste für die gesamte Community ist + +Beispiele für nicht akzeptables Verhalten sind: + +* Die Verwendung sexualisierter bzw. anstößiger Sprache oder Bilder + sowie sexuelle Aufmerksamkeit oder Annäherungsversuche jeglicher Art +* Trolling, beleidigende oder herabwürdigende Kommentare + sowie persönliche oder politische Angriffe +* Öffentliche sowie private Belästigung +* Das Teilen privater Informationen anderer Leute ohne deren explizite Zustimmung, + wie bspw. die physische oder die E-Mail-Adresse +* Anderes Verhalten, das in einem professionellen Umfeld begründeter Weise als + unangemessen angesehen werden könnte + +## Durchsetzungsbefugnisse + +Die Leiter der Community sind dafür verantwortlich, unsere Standards für +akzeptables Verhalten zu klären und durchzusetzen und werden angemessene +und faire Korrekturmaßnahmen ergreifen, wenn sie ein Verhalten als unangemessen, +bedrohlich, beleidigend oder schädlich erachten. + +Die Leiter der Community haben das Recht und die Pflicht, Kommentare, Commits, +Code, Wiki-Bearbeitungen, Issues und andere Beiträge, die nicht mit dem +Verhaltenskodex vereinbar sind, zu entfernen, zu bearbeiten oder abzulehnen. +Sie werden, falls angebracht, die Gründe für Moderationsentscheidungen mitteilen. + +## Geltungsbereich + +Dieser Verhaltenskodex gilt in allen Community-Bereichen und auch dann, wenn +eine Person die Community offiziell in öffentlichen Bereichen vertritt. +Beispiele für die Vertretung unserer Community sind die Verwendung einer +offiziellen E-Mail-Adresse, das Posten über einen offiziellen +Social-Media-Account oder die Tätigkeit als ernannter +Vertreter bei einer Online- oder Präsenzveranstaltung. + +## Geltendmachung + +Fälle von missbräuchlichem, belästigendem oder anderweitig inakzeptablem Verhalten können +den für die Durchsetzung zuständigen Community-Leitern +unter [info@rustdesk.com](mailto:info@rustdesk.com) gemeldet werden. +Jeder Fall wird umgehend und fair geprüft und untersucht. + +## Richtlinien zur Geltendmachung + +Die Community-Leiter werden die folgenden Community-Auswirkungsrichtlinien befolgen, +um die Konsequenzen für jede Handlung zu bestimmen, die sie als Verstoß gegen diesen +Verhaltenskodex ansehen: + +### 1. Korrektur + +**Auswirkungen auf die Community**: Verwendung unangemessener Sprache oder anderes +Verhalten, welches als unprofessionell oder in der Community unerwünscht angesehen wird. + +**Konsequenz**: Eine private, schriftliche Verwarnung durch die Leiter der Community, +in der die Art des Verstoßes klar dargelegt und erklärt wird, warum das +Verhalten unangemessen war. Eine öffentliche Entschuldigung kann verlangt werden. + +### 2. Warnung + +**Auswirkungen auf die Community**: Ein Verstoß durch einen einzelnen Vorfall +oder eine Reihe von Handlungen. + +**Konsequenz**: Eine Verwarnung mit Konsequenzen für das weitere Verhalten. Keine +Interaktion mit den beteiligten Personen, einschließlich unaufgeforderter Interaktion mit +denjenigen, die den Verhaltenskodex durchsetzen, für einen bestimmten Zeitraum. Dies +schließt die Vermeidung von Interaktionen in Gemeinschaftsräumen sowie externen Kanälen +wie sozialen Medien ein. Ein Verstoß gegen diese Bedingungen kann zu einer vorübergehenden oder +dauerhaften Sperrung führen. + +### 3. Temporärer Sperrung + + +**Auswirkungen auf die Community**: Ein schwerwiegender Verstoß gegen die Community-Standards, +einschließlich anhaltend unangemessenem Verhalten. + +**Konsequenz**: Eine vorübergehende Sperrung jeglicher Art von Interaktion oder öffentlicher +Kommunikation mit der Community für einen bestimmten Zeitraum. Während dieses Zeitraums sind +keine öffentlichen oder privaten Interaktionen mit den betroffenen Personen, +einschließlich unaufgeforderter Interaktionen mit denjenigen, +die den Verhaltenskodex durchsetzen, erlaubt. +Ein Verstoß gegen diese Bedingungen kann zu einer dauerhaften Sperrung führen. + +### 4. Dauerhafte Sperrung + +**Auswirkungen auf die Community**: Wiederholte Verstöße gegen die Community-Standards, +einschließlich anhaltend unangemessenem Verhalten, Belästigung einer +Person oder Aggression gegenüber oder Herabwürdigung von Personengruppen. + +**Konsequenz**: Ein dauerhafter Ausschluss von jeglicher öffentlicher +Interaktion innerhalb der Community. + +## Quellenangabe + +Dieser Verhaltenskodex ist eine Adaption des [Contributor Covenant][homepage], +Version 2.0, verfügbar unter +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Die Richtlinien zu den Auswirkungen auf die Gemeinschaft wurden inspiriert von +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +Für Antworten auf häufig gestellte Fragen zu diesem Verhaltenskodex siehe die +häufig gestellten Fragen (FAQ) unter +[https://www.contributor-covenant.org/faq][FAQ]. Übersetzungen sind verfügbar +unter [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-JP.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-JP.md new file mode 100644 index 0000000..5b21624 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-JP.md @@ -0,0 +1,101 @@ + +# コントリビューターè¦ç´„ 行動è¦ç¯„ + +## ç§ãŸã¡ã®èª“ã„ + +ç§ãŸã¡ã¯ã€ãƒ¡ãƒ³ãƒãƒ¼ã€è²¢çŒ®è€…ã€ãƒªãƒ¼ãƒ€ãƒ¼ã¨ã—ã¦ã€å¹´é½¢ã€ä½“æ ¼ã€ç›®ã«è¦‹ãˆã‚‹ãƒ»è¦‹ãˆãªã„障害〠+æ°‘æ—æ€§ã€æ€§ã®ç‰¹å¾´ã€æ€§è‡ªèªã¨è¡¨ç¾ã€çµŒé¨“ã®ãƒ¬ãƒ™ãƒ«ã€æ•™è‚²ã€ç¤¾ä¼šçµŒæ¸ˆçš„地ä½ã€å›½ç±ã€å€‹äººã®å¤–見〠+人種ã€å®—æ•™ã€æ€§çš„自èªã¨æŒ‡å‘ã«é–¢ä¿‚ãªãã€èª°ã‚‚ãŒãƒãƒ©ã‚¹ãƒ¡ãƒ³ãƒˆã®ãªã„コミュニティã«å‚加ã§ãるよã†ã«ã™ã‚‹ã“ã¨ã‚’誓ã„ã¾ã™ã€‚ + +ç§ãŸã¡ã¯ã€é–‹ã‹ã‚ŒãŸã€æ­“迎ã•れãŸã€å¤šæ§˜ã§ã€åŒ…容力ã®ã‚ã‚‹ã€å¥å…¨ãªåœ°åŸŸç¤¾ä¼šã«è²¢çŒ®ã™ã‚‹ã‚ˆã†ã«è¡Œå‹•ã—ã€äº¤æµã™ã‚‹ã“ã¨ã‚’誓ã„ã¾ã™ã€‚ + +## ç§ãŸã¡ã®åŸºæº– + +地域社会ã«ã¨ã£ã¦å¥½ã¾ã—ã„環境ã«ã‚³ãƒ³ãƒˆãƒªãƒ“ュートã™ã‚‹è¡Œå‹•ã®ä¾‹ã«ã¯ã€ä»¥ä¸‹ã®ã‚ˆã†ãªã‚‚ã®ãŒã‚ã‚‹: + +* 他者ã¸ã®å…±æ„Ÿã¨å„ªã—ã• +* ç•°ãªã‚‹æ„見ã€è¦–点ã€çµŒé¨“ã‚’å°Šé‡ã™ã‚‹ã“㨠+* 建設的ãªãƒ•ィードãƒãƒƒã‚¯ã‚’与ãˆã€æ½”ãå—ã‘入れるã“㨠+* ç§ãŸã¡ã®éŽã¡ã«ã‚ˆã£ã¦å½±éŸ¿ã‚’å—ã‘ãŸäººã€…ã«è²¬ä»»ã‚’å—ã‘入れã€è¬ç½ªã—ã€çµŒé¨“ã‹ã‚‰å­¦ã¶ã“㨠+* ç§ãŸã¡å€‹äººã«ã¨ã£ã¦ã ã‘ã§ãªãã€åœ°åŸŸç¤¾ä¼šå…¨ä½“ã«ã¨ã£ã¦ä½•ãŒæœ€å–„ã§ã‚ã‚‹ã‹ã«ç„¦ç‚¹ã‚’åˆã‚ã›ã‚‹ã“㨠+ +許ã•れãªã„行為ã®ä¾‹: + +* 性的ãªè¨€è‘‰ã‚„イメージã®ä½¿ç”¨ã€æ€§çš„ãªæ³¨ç›®ã‚„誘ã„ã‹ã‘ +* è’らã—ã€ä¾®è¾±çš„ã¾ãŸã¯è»½è”‘çš„ãªã‚³ãƒ¡ãƒ³ãƒˆã€å€‹äººçš„ã¾ãŸã¯æ”¿æ²»çš„ãªæ”»æ’ƒ +* 公的ã¾ãŸã¯ç§çš„ãªå«ŒãŒã‚‰ã› +* 明示的ãªè¨±å¯ãªãã€ä»–人ã®ä½æ‰€ã‚„é›»å­ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ãªã©ã®å€‹äººæƒ…報を公開ã™ã‚‹ã“㨠+* è·æ¥­ä¸Šä¸é©åˆ‡ã¨è¦‹ãªã•れるãã®ä»–ã®è¡Œç‚º + +## 執行責任 + +コミュニティリーダーã¯ã€è¨±å®¹ã•れる行動ã®åŸºæº–を明確ã«ã—ã€å®Ÿæ–½ã™ã‚‹è²¬ä»»ãŒã‚り〠+ä¸é©åˆ‡ã€è„…è¿«çš„ã€æ”»æ’ƒçš„ã€ã¾ãŸã¯æœ‰å®³ã¨åˆ¤æ–­ã•れる行動ã«å¯¾ã—ã¦ã¯ã€é©åˆ‡ã‹ã¤å…¬æ­£ãªæ˜¯æ­£æŽªç½®ã‚’ã¨ã‚Šã¾ã™ + +コミュニティリーダーã¯ã€æœ¬è¡Œå‹•è¦ç¯„ã«æ²¿ã‚ãªã„コメントã€ã‚³ãƒŸãƒƒãƒˆã€ã‚³ãƒ¼ãƒ‰ã€ã‚¦ã‚£ã‚­ç·¨é›†ã€ +課題ã€ãã®ä»–ã®è²¢çŒ®ã‚’削除ã€ç·¨é›†ã€æ‹’å¦ã™ã‚‹æ¨©åˆ©ã¨è²¬ä»»ã‚’有ã—ã€é©åˆ‡ãªå ´åˆã«ã¯ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³æ±ºå®šã®ç†ç”±ã‚’ä¼ãˆã¾ã™ã€‚ + +## スコープ + +ã“ã®è¡Œå‹•è¦ç¯„ã¯ã€ã™ã¹ã¦ã®ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ã‚¹ãƒšãƒ¼ã‚¹ã§é©ç”¨ã•れã€ã¾ãŸå€‹äººãŒå…¬çš„ãªã‚¹ãƒšãƒ¼ã‚¹ã§ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ã‚’å…¬å¼ã«ä»£è¡¨ã—ã¦ã„ã‚‹å ´åˆã«ã‚‚é©ç”¨ã•れã¾ã™ã€‚ +当コミュニティを代表ã™ã‚‹ä¾‹ã¨ã—ã¦ã¯ã€å…¬å¼ E メールアドレスã®ä½¿ç”¨ã€å…¬å¼ã‚½ãƒ¼ã‚·ãƒ£ãƒ«ãƒ¡ãƒ‡ã‚£ã‚¢ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚ˆã‚‹æŠ•稿〠+オンラインã¾ãŸã¯ã‚ªãƒ•ラインã®ã‚¤ãƒ™ãƒ³ãƒˆã§ã®ä»»å‘½ã•れãŸä»£è¡¨ã¨ã—ã¦ã®è¡Œå‹•ãªã©ãŒæŒ™ã’られã¾ã™ã€‚ + +## 施行 + +è™å¾…ã€ãƒãƒ©ã‚¹ãƒ¡ãƒ³ãƒˆã€ãã®ä»–容èªã§ããªã„行為ãŒã‚ã£ãŸå ´åˆã¯ã€[info@rustdesk.com](mailto:info@rustdesk.com) ã® +執行担当コミュニティリーダーã«å ±å‘Šã™ã‚‹ã“ã¨ãŒã§ãる。 +ã™ã¹ã¦ã®è‹¦æƒ…ã¯ã€è¿…速ã‹ã¤å…¬æ­£ã«æ¤œè¨Žãƒ»èª¿æŸ»ã•れã¾ã™ã€‚ + +ã™ã¹ã¦ã®åœ°åŸŸç¤¾ä¼šã®æŒ‡å°Žè€…ã¯ã€ã„ã‹ãªã‚‹äº‹ä»¶ã®å ±å‘Šè€…ã®ãƒ—ライãƒã‚·ãƒ¼ã¨å®‰å…¨ã‚’å°Šé‡ã™ã‚‹ç¾©å‹™ãŒã‚る。 + +## 執行ガイドライン + +コミュニティリーダーã¯ã€æœ¬è¡Œå‹•è¦ç¯„ã«é•åã™ã‚‹ã¨åˆ¤æ–­ã—ãŸè¡Œç‚ºã«å¯¾ã™ã‚‹çµæžœã‚’決定ã™ã‚‹éš›ã€ +以下ã®ã€Œã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£ã¸ã®å½±éŸ¿ã«é–¢ã™ã‚‹ã‚¬ã‚¤ãƒ‰ãƒ©ã‚¤ãƒ³ã€ã«å¾“ã„ã¾ã™: + +### 1. 修正 + +**コミュニティã¸ã®å½±éŸ¿**: ä¸é©åˆ‡ãªè¨€è‘‰ã®ä½¿ç”¨ã€ã¾ãŸã¯ãƒ—ロフェッショナルã§ãªã„ã€ã‚ã‚‹ã„ã¯åœ°åŸŸç¤¾ä¼šã§æ­“迎ã•れãªã„ã¨ã¿ãªã•れるãã®ä»–ã®è¡Œå‹•。 + +**çµæžœ**: コミュニティリーダーã‹ã‚‰ã®ç§çš„ãªæ›¸é¢ã«ã‚ˆã‚‹è­¦å‘Šã€‚é•åã®æ€§è³ªã¨ã€ +ãªãœãã®è¡Œç‚ºãŒä¸é©åˆ‡ã§ã‚ã£ãŸã®ã‹ã«ã¤ã„ã¦ã®èª¬æ˜Žã‚’明確ã«ã™ã‚‹ã€‚公的ãªè¬ç½ªãŒè¦æ±‚ã•れる場åˆã‚‚ã‚る。 + +### 2. 警告 + +**コミュニティã¸ã®å½±éŸ¿**: å˜ä¸€ã®å‡ºæ¥äº‹ã¾ãŸã¯ä¸€é€£ã®è¡Œå‹•ã«ã‚ˆã‚‹é•å。 + +**çµæžœ**: 行動を続ã‘ãŸå ´åˆã®çµæžœã‚’ä¼´ã†è­¦å‘Šã€‚一定期間ã€è¡Œå‹•è¦ç¯„ã®å®Ÿæ–½è€…ã¨ã®å‹æ‰‹ãªäº¤æµã‚’å«ã‚〠+関係者ã¨äº¤æµã—ãªã„ã“ã¨ã€‚ã“れã«ã¯ã€ã‚½ãƒ¼ã‚·ãƒ£ãƒ«ãƒ¡ãƒ‡ã‚£ã‚¢ãªã©ã®å¤–部ãƒãƒ£ãƒ³ãƒãƒ«ã ã‘ã§ãªã〠+コミュニティスペースã§ã®äº¤æµã‚’é¿ã‘ã‚‹ã“ã¨ã‚‚å«ã¾ã‚Œã¾ã™ã€‚ã“ã‚Œã‚‰ã®æ¡ä»¶ã«é•åã—ãŸå ´åˆã€ä¸€æ™‚çš„ã¾ãŸã¯æ’ä¹…çš„ã«è¿½æ”¾ã•れるå¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚ + +### 3. 一時的ãªç¦æ­¢ + +**コミュニティã¸ã®å½±éŸ¿**: 継続的ãªä¸é©åˆ‡ãªè¡Œå‹•ã‚’å«ã‚€ã€ã‚³ãƒŸãƒ¥ãƒ‹ãƒ†ã‚£åŸºæº–ã«å¯¾ã™ã‚‹é‡å¤§ãªé•å。 + +**çµæžœ**: 一定期間ã€åœ°åŸŸç¤¾ä¼šã¨ã®ã‚らゆる交æµã‚„公的ãªã‚³ãƒŸãƒ¥ãƒ‹ã‚±ãƒ¼ã‚·ãƒ§ãƒ³ã‚’一時的ã«ç¦æ­¢ã™ã‚‹ã“ã¨ã€‚ +ã“ã®æœŸé–“中ã¯ã€è¡Œå‹•è¦ç¯„を執行ã™ã‚‹äººã€…ã¨ã®æœªæ‰¿è«¾ã®äº¤æµã‚’å«ã‚ã€é–¢ä¿‚者ã¨ã®å…¬ç§ã«ã‚ãŸã‚‹äº¤æµã¯è¨±ã•れãªã„。 +ã“ã‚Œã‚‰ã®æ¡ä»¶ã«é•åã—ãŸå ´åˆã€æ°¸ä¹…ç¦æ­¢ã¨ãªã‚‹å¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚ + +### 4. æ°¸ä¹…ç¦æ­¢ + +**コミュニティã¸ã®å½±éŸ¿**: 継続的ãªä¸é©åˆ‡ãªè¡Œå‹•ã€å€‹äººã«å¯¾ã™ã‚‹å«ŒãŒã‚‰ã›ã€ +ã¾ãŸã¯å€‹äººã‚¯ãƒ©ã‚¹ã«å¯¾ã™ã‚‹æ”»æ’ƒã‚„中傷ãªã©ã€åœ°åŸŸç¤¾ä¼šã®åŸºæº–ã«å¯¾ã™ã‚‹é•åã®ãƒ‘ターンを示ã™ã“ã¨ã€‚ + +**çµæžœ**: コミュニティ内ã§ã®ã‚らゆる公的交æµã®æ°¸ä¹…ç¦æ­¢ã€‚ + +## 帰属 + +ã“ã®è¡Œå‹•è¦ç¯„ã¯ã€[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0] ã«æŽ²è¼‰ã•れã¦ã„ã‚‹ +[コントリビューターè¦ç´„][ホームページ]ã€ãƒãƒ¼ã‚¸ãƒ§ãƒ³ 2.0 ã‹ã‚‰å¼•用ã—ãŸã‚‚ã®ã§ã™ã€‚ + +コミュニティインパクトガイドラインã¯ã€[Mozilla's code of conduct enforcement ladder][Mozilla CoC] ã«è§¦ç™ºã•れã¾ã—ãŸã€‚ + +ã“ã®è¡Œå‹•è¦ç¯„ã«é–¢ã™ã‚‹ã‚ˆãã‚る質å•ã«ã¤ã„ã¦ã¯ã€[https://www.contributor-covenant.org/faq][FAQ] ã® FAQ ã‚’ã”覧ãã ã•ã„。 +翻訳㯠[https://www.contributor-covenant.org/translations][翻訳] ã«ã‚りã¾ã™ã€‚ + +[ホームページ]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[翻訳]: https://www.contributor-covenant.org/translations diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-KR.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-KR.md new file mode 100644 index 0000000..40fea02 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-KR.md @@ -0,0 +1,133 @@ + +# ê¸°ì—¬ìž ê³„ì•½ í–‰ë™ ê°•ë ¹ + +## ìš°ë¦¬ì˜ ì„œì•½ + +회ì›, 기여ìž, 리ë”로서 우리는 나ì´, ì‹ ì²´ í¬ê¸°, ëˆˆì— +ë³´ì´ê±°ë‚˜ ë³´ì´ì§€ 않는 장애, 민족, 성 특성, 성 정체성 ë° +표현, 경험 수준, êµìœ¡, 사회 ê²½ì œì  ì§€ìœ„, êµ­ì , 외모, +ì¸ì¢…, 종êµ, ì„±ì  ì •ì²´ì„± ë° ì§€í–¥ì— ê´€ê³„ì—†ì´ ëª¨ë“  ì‚¬ëžŒì´ +괴롭힘 ì—†ì´ ì»¤ë®¤ë‹ˆí‹°ì— ì°¸ì—¬í•  수 있ë„ë¡ í•  ê²ƒì„ +서약합니다. + +우리는 개방ì ì´ê³  환ì˜í•˜ë©° 다양하고 í¬ìš©ì ì´ë©° 건강한 ì»¤ë®¤ë‹ˆí‹°ì— +기여하는 ë°©ì‹ìœ¼ë¡œ í–‰ë™í•˜ê³  êµë¥˜í•  ê²ƒì„ ì•½ì†í•©ë‹ˆë‹¤. + +## ìš°ë¦¬ì˜ í‘œì¤€ + +ì»¤ë®¤ë‹ˆí‹°ì˜ ê¸ì •ì ì¸ í™˜ê²½ì— ê¸°ì—¬í•˜ëŠ” í–‰ë™ì˜ 예는 +다ìŒê³¼ 같습니다: + +* 다른 사람들ì—게 ê³µê°ê³¼ ì¹œì ˆì„ ë³´ì—¬ì£¼ê¸° +* 다양한 ì˜ê²¬, ê´€ì , ê²½í—˜ì„ ì¡´ì¤‘í•˜ê¸° +* 건설ì ì¸ í”¼ë“œë°±ì„ ì œê³µí•˜ê³  우아하게 받아들ì´ê¸° +* ìš°ë¦¬ì˜ ì‹¤ìˆ˜ë¡œ ì¸í•´ ì˜í–¥ì„ ë°›ì€ ì‚¬ëžŒë“¤ì—게 ì±…ìž„ì„ ë°›ì•„ë“¤ì´ê³  사과하며 + ê·¸ ê²½í—˜ì„ í†µí•´ 배우기 +* 우리 ê°œì¸ë¿ë§Œ ì•„ë‹ˆë¼ ì „ì²´ ì»¤ë®¤ë‹ˆí‹°ì— ê°€ìž¥ ì¢‹ì€ ê²ƒì´ ë¬´ì—‡ì¸ì§€ + 집중하기 + +용납할 수 없는 í–‰ë™ì˜ 예는 다ìŒê³¼ 같습니다: + +* 성ì ì¸ 언어 ë˜ëŠ” ì´ë¯¸ì§€ì˜ 사용, 모든 ì¢…ë¥˜ì˜ ì„±ì  ê´€ì‹¬ ë˜ëŠ” + ì ‘ê·¼ 행위 +* 트롤ë§, 모욕ì ì´ê±°ë‚˜ 경멸ì ì¸ 댓글, ê°œì¸ì  ë˜ëŠ” ì •ì¹˜ì  ê³µê²© +* ê³µê°œì  ë˜ëŠ” 사ì ì¸ 괴롭힘 +* 명시ì ì¸ 허가 ì—†ì´ íƒ€ì¸ì˜ 실제 주소 ë˜ëŠ” ì´ë©”ì¼ ì£¼ì†Œì™€ ê°™ì€ + ê°œì¸ì •보를 게시하는 행위 +* ì§ì—…ì  í™˜ê²½ì—서 합리ì ìœ¼ë¡œ ë¶€ì ì ˆí•˜ë‹¤ê³  ê°„ì£¼ë  ìˆ˜ 있는 + 기타 행위 + +## 시행 ì±…ìž„ + +커뮤니티 리ë”는 허용ë˜ëŠ” í–‰ë™ì˜ ê¸°ì¤€ì„ ëª…í™•ížˆ 하고 시행할 +ì±…ìž„ì´ ìžˆìœ¼ë©° ë¶€ì ì ˆí•˜ê±°ë‚˜ 위협ì ì´ê±°ë‚˜ 모욕ì ì´ê±°ë‚˜ +유해하다고 íŒë‹¨ë˜ëŠ” í–‰ë™ì— 대해 ì ì ˆí•˜ê³  공정한 시정 조치를 +취합니다. + +커뮤니티 리ë”는 본 í–‰ë™ ê°•ë ¹ì— ë¶€í•©í•˜ì§€ 않는 댓글, 커밋, +코드, 위키 편집, ì´ìŠˆ ë° ê¸°íƒ€ 기여를 ì‚­ì œ, 편집 ë˜ëŠ” 거부할 +권한과 ì±…ìž„ì´ ìžˆìœ¼ë©°, ì ì ˆí•œ 경우 중재 ê²°ì •ì˜ ì´ìœ ë¥¼ +전달합니다. + +## 범위 + +본 í–‰ë™ ê°•ë ¹ì€ ëª¨ë“  커뮤니티 공간ì—서 ì ìš©ë˜ë©°, ê°œì¸ì´ 공개 +공간ì—서 커뮤니티를 ê³µì‹ì ìœ¼ë¡œ 대표하는 경우ì—ë„ ì ìš©ë©ë‹ˆë‹¤. +커뮤니티를 대표하는 예로는 ê³µì‹ ì´ë©”ì¼ ì£¼ì†Œ 사용, ê³µì‹ ì†Œì…œ 미디어 +ê³„ì •ì„ í†µí•œ 게시, 온ë¼ì¸ ë˜ëŠ” 오프ë¼ì¸ ì´ë²¤íЏì—서 ì§€ì •ëœ ëŒ€í‘œìžë¡œ +활ë™í•˜ëŠ” 것 ë“±ì´ ìžˆìŠµë‹ˆë‹¤. + +## 시행 + +모욕ì , 괴롭힘 ë˜ëŠ” 기타 용납할 수 없는 í–‰ë™ì€ + [info@rustdesk.com](mailto:info@rustdesk.com)으로 법 ì§‘í–‰ì„ ë‹´ë‹¹í•˜ëŠ” 커뮤니티 리ë”ì—게 +신고하실 수 있습니다. +모든 불만 ì‚¬í•­ì€ ì‹ ì†í•˜ê³  공정하게 검토 ë° ì¡°ì‚¬ë©ë‹ˆë‹¤. + +모든 커뮤니티 리ë”는 모든 사건 ì‹ ê³ ìžì˜ 사ìƒí™œê³¼ ë³´ì•ˆì„ ì¡´ì¤‘í•  ì˜ë¬´ê°€ +있습니다. + +## 시행 지침 + +커뮤니티 리ë”는 ì´ í–‰ë™ ê°•ë ¹ì„ ìœ„ë°˜í•œ 것으로 간주ë˜ëŠ” 모든 í–‰ë™ì— 대한 +결과를 ê²°ì •í•  때 ë‹¤ìŒ ì»¤ë®¤ë‹ˆí‹° ì˜í–¥ ì§€ì¹¨ì„ ë”°ë¦…ë‹ˆë‹¤: + +### 1. 수정 + +**커뮤니티 ì˜í–¥**: 커뮤니티ì—서 비전문ì ì´ê±°ë‚˜ 환ì˜ë°›ì§€ 못하는 +것으로 간주ë˜ëŠ” ë¶€ì ì ˆí•œ 언어 사용ì´ë‚˜ 기타 행위입니다. + +**ê²°ê³¼**: 커뮤니티 리ë”ì˜ ë¹„ê³µê°œ 서면 경고. 위반 ì‚¬í•­ì˜ ì„±ê²©ê³¼ +해당 í–‰ë™ì´ ë¶€ì ì ˆí–ˆë˜ ì´ìœ ë¥¼ 명확히 설명해야 합니다. +공개 사과를 요청할 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤. + +### 2. 경고 + +**커뮤니티 ì˜í–¥**: ë‹¨ì¼ ì‚¬ê±´ ë˜ëŠ” ì¼ë ¨ì˜ 행위를 통한 +위반입니다. + +**ê²°ê³¼**: ì§€ì†ì ì¸ í–‰ë™ì— 대한 경고 ë° ê²°ê³¼. í–‰ë™ ê°•ë ¹ 시행 담당ìžì™€ì˜ +ì›ì¹˜ 않는 ìƒí˜¸ìž‘ìš©ì„ í¬í•¨í•˜ì—¬ 관련ìžì™€ì˜ ìƒí˜¸ìž‘ìš©ì€ ì¼ì • +기간 ë™ì•ˆ 금지ë©ë‹ˆë‹¤. 여기ì—는 ê³µë™ ê³µê°„ ë° ì†Œì…œ 미디어와 +ê°™ì€ ì™¸ë¶€ 채ë„ì—ì„œì˜ ìƒí˜¸ìž‘ìš© 금지가 í¬í•¨ë©ë‹ˆë‹¤. ì´ëŸ¬í•œ +ì¡°ê±´ì„ ìœ„ë°˜í•  경우 ì¼ì‹œì  ë˜ëŠ” ì˜êµ¬ì ìœ¼ë¡œ ì´ìš©ì´ ê¸ˆì§€ë  ìˆ˜ +있습니다. + +### 3. ì¼ì‹œ 금지 + +**커뮤니티 ì˜í–¥**: ì§€ì†ì ì¸ ë¶€ì ì ˆí•œ í–‰ë™ì„ í¬í•¨í•˜ì—¬ +커뮤니티 ê¸°ì¤€ì„ ì‹¬ê°í•˜ê²Œ 위반한 경우입니다. + +**ê²°ê³¼**: ì¼ì • 기간 ë™ì•ˆ ì»¤ë®¤ë‹ˆí‹°ì™€ì˜ ëª¨ë“  ìƒí˜¸ìž‘ìš©ì´ë‚˜ 공개ì ì¸ ì†Œí†µì´ +ì¼ì‹œì ìœ¼ë¡œ 금지ë©ë‹ˆë‹¤. ì´ ê¸°ê°„ ë™ì•ˆì—는 í–‰ë™ ê°•ë ¹ì„ ì‹œí–‰í•˜ëŠ” +ì‚¬ëžŒë“¤ê³¼ì˜ ì›ì¹˜ 않는 ìƒí˜¸ìž‘ìš©ì„ í¬í•¨í•˜ì—¬ 관련ìžë“¤ê³¼ì˜ ê³µê°œì  ë˜ëŠ” +사ì ì¸ ìƒí˜¸ìž‘ìš©ì´ í—ˆìš©ë˜ì§€ 않습니다. +ì´ëŸ¬í•œ ì¡°ê±´ì„ ìœ„ë°˜í•  경우 ì˜êµ¬ì ìœ¼ë¡œ ì´ìš©ì´ ê¸ˆì§€ë  ìˆ˜ 있습니다. + +### 4. ì˜êµ¬ 금지 + +**커뮤니티 ì˜í–¥**: ì§€ì†ì ì¸ ë¶€ì ì ˆí•œ í–‰ë™, 특정 ê°œì¸ì— 대한 괴롭힘, +특정 ê³„ì¸µì— ëŒ€í•œ 공격성 ë˜ëŠ” 비하 등 ê³µë™ì²´ ê¸°ì¤€ì„ ìœ„ë°˜í•˜ëŠ” +í–‰ë™ì„ ë³´ì´ëŠ” 경우입니다. + +**ê²°ê³¼**: ê³µë™ì²´ ë‚´ 모든 ì¢…ë¥˜ì˜ ê³µê°œì ì¸ ìƒí˜¸ìž‘ìš©ì´ ì˜êµ¬ì ìœ¼ë¡œ +금지ë©ë‹ˆë‹¤. + +## ê·€ì† + +본 í–‰ë™ ê°•ë ¹ì€ [Contributor Covenant][homepage] 버전 2.0ì„ ë°”íƒ•ìœ¼ë¡œ 작성ë˜ì—ˆìœ¼ë©° +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]ì—서 + 확ì¸í•˜ì‹¤ 수 있습니다. + +커뮤니티 ì˜í–¥ ì§€ì¹¨ì€ +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]ì—서 ì˜ê°ì„ 받았습니다. + +본 í–‰ë™ ê°•ë ¹ì— ëŒ€í•œ ì¼ë°˜ì ì¸ ì§ˆë¬¸ì€ [https://www.contributor-covenant.org/faq][FAQ]ì—서 FAQ를 +참조하세요. ë²ˆì—­ì€ [https://www.contributor-covenant.org/translations][translations]ì—서 +확ì¸í•˜ì‹¤ 수 있습니다. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-NL.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-NL.md new file mode 100644 index 0000000..49923a2 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-NL.md @@ -0,0 +1,136 @@ + +# Gedragscode Overeenkomst Medewerkers + +## Onze Belofte + +Wij als leden, medewerkers en leiders beloven deelname aan onze +gemeenschap een pesterij-vrije ervaring te maken voor iedereen, ongeacht leeftijd, lichaamsgrootte, +zichtbare of onzichtbare handicap, etniciteit, geslachtskenmerken, gender +identiteit en expressie, ervaringsniveau, opleiding, sociaal-economische status, +nationaliteit, persoonlijk voorkomen, ras, religie of seksuele identiteit +en geaardheid. + +Wij beloven te handelen en met elkaar om te gaan op manieren die bijdragen aan een open, gastvrije, +diverse, inclusieve en gezonde gemeenschap. + +## Onze Normen + +Voorbeelden van gedrag dat bijdraagt tot een positieve omgeving voor onze +gemeenschap omvatten: + +* Medeleven en vriendelijkheid tonen tegenover andere mensen +* Respect hebben voor verschillende meningen, standpunten en ervaringen +* Constructieve feedback geven en met dank aanvaarden +* Verantwoordelijkheid accepteren en excuses aanbieden aan degenen die door onze fouten zijn getroffen, + en leren van de ervaring +* Focussen op wat het beste is, niet alleen voor ons als individu, maar voor de + totale gemeenschap + +Voorbeelden van onaanvaardbaar gedrag zijn: + +* Het gebruik van seksueel getinte taal of beelden, en seksuele aandacht of + alle soorten avances +* Treiteren, beledigende of denigrerende opmerkingen en persoonlijke of politieke aanvallen. +* Openbare of persoonlijke intimidatie +* Publiceren van andermans persoonlijke informatie, zoals een fysiek adres of e-mail, + zonder hun uitdrukkelijke toestemming +* Ander gedrag dat normaal als ongepast kan worden beschouwd in een + professionele omgeving + +## Verantwoordelijkheden inzake Handhaving + +De leiders van de Gemeenschap zijn verantwoordelijk voor het verduidelijken +en handhaven van onze normen voor aanvaardbaar gedrag en zullen passende +en billijke corrigerende maatregelen nemen als reactie op gedrag dat zij ongepast, +bedreigend, beledigend of schadelijk achten. + +Leiders van de Gemeenschap hebben het recht en de verantwoordelijkheid om +commentaar, bijdragen, code, wikibewerkingen, issues en andere bijdragen die +niet in overeenstemming zijn met deze Gedragscode te verwijderen, te bewerken of +af te wijzen, en zullen de redenen voor moderatiebeslissingen zo nodig meedelen. + +## Toepassingsgebied + +Deze Gedragscode geldt binnen alle gemeenschapsruimtes en is ook van toepassing +wanneer iemand de gemeenschap officieel vertegenwoordigt in openbare ruimtes. +Voorbeelden van het vertegenwoordigen van onze gemeenschap zijn het gebruik van +een officieel e-mailadres, het posten via een officieel sociaal media-account of het +optreden als aangewezen vertegenwoordiger bij een online of offline evenement. + +## Handhaving + +Gevallen van beledigend, intimiderend of anderszins onaanvaardbaar gedrag kunnen +worden gemeld aan de gemeenschapsleiders die verantwoordelijk zijn voor de +handhaving op [info@rustdesk.com](mailto:info@rustdesk.com). +Alle klachten zullen snel en eerlijk worden onderzocht. + +Alle leiders van de gemeenschap zijn verplicht de privacy en de veiligheid van +de melder van een incident te respecteren. + +## Handhaving Richtlijnen + +De leiders van de Gemeenschap volgen deze Communautaire Impact Richtlijnen bij +het bepalen van de consequenties voor elke actie die zij in strijd achten +met deze Gedragscode: + +### 1. Rechtzetting + +**Gevolgen Gemeenschap**: Gebruik van ongepast taalgebruik of ander gedrag +dat onprofessioneel of ongewenst wordt geacht in de gemeenschap. + +**Gevolgen**: Een persoonlijke, schriftelijke waarschuwing van de leiders van +de gemeenschap, met duidelijkheid over de aard van de overtreding en een +uitleg waarom het gedrag ongepast was. +Een publieke verontschuldiging kan worden gevraagd. + +### 2. Waarschuwing + +**Gevolgen Gemeenschap**: Een overtreding door een enkel incident of +een reeks handelingen. + +**Gevolgen**: Geen interactie met de betrokken personen, inclusief +ongevraagde interactie met degenen die de Gedragscode handhaven, +gedurende een bepaalde periode. Dit omvat het vermijden van interacties +in gemeenschapsruimtes en externe kanalen zoals sociale media. +Overtreding van deze voorwaarden kan leiden tot een tijdelijke +of permanente uitsluiting. + +### 3. Tijdelijke Uitsluiting + +**Gevolgen Gemeenschap**: Een ernstige schending van de +gemeenschapsnormen, waaronder aanhoudend ongepast gedrag. + +**Gevolgen**: Een tijdelijk verbod op elke vorm van interactie +of openbare communicatie met de gemeenschap voor een bepaalde + periode. Geen openbare of private interactie met de betrokkenen, + inclusief ongevraagde interactie met degenen die de gedragscode + handhaven, is gedurende deze periode toegestaan. + Overtreding van deze voorwaarden kan leiden tot een permanente uitsluiting. + +### 4. Permanente Uitsluiting + +**Gevolgen Gemeenschap**: Aantonen van een patroon van schending van +de gemeenschapsnormen, waaronder aanhoudend ongepast gedrag, intimidatie +van een individu, of agressie tegen of vernedering van klassen van individuen. + +**Gevolgen**: Een permanente uitsluiting van elke vorm van publieke interactie +binnen de gemeenschap. + +## Naamsvermelding + +Deze gedragscode is overgenomen uit de [Bijdrager Overeenkomst][homepagina], +versie 2.0, beschikbaar op +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +De Invloed op Richtlijnen voor Gemeenschap zijn gebaseerd op +[Mozilla's gedragscode handhavingslijst][Mozilla CoC]. + +Voor antwoorden op veelgestelde vragen over deze gedragscode, zie de FAQ op +[https://www.contributor-covenant.org/faq][FAQ]. Vertalingen zijn beschikbaar +op [https://www.contributor-covenant.org/translations][translations]. + +[homepagina]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[vertalingen]: https://www.contributor-covenant.org/translations diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-NO.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-NO.md new file mode 100644 index 0000000..baefda0 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-NO.md @@ -0,0 +1,125 @@ + +# Atferdskodeks for bidragsyterpaktern + +## Hva Vi StÃ¥r For + +Vi som medlemer, bidragere, og ledere stÃ¥r for Ã¥ skape ett hat-fritt felleskap, +uansett alder, kroppstørrelse, synlig eller usynlige funksjonsnedsettninger, +etnesitet, kjønns karaktertrekk, kjønnsidentitet, kunnskapsnivÃ¥, utdanning, +sosial-økonomisk status, nasjonalitet, utsende, rase, religion, eller seksual +identitet og orientasjon. + +Vi stÃ¥r for Ã¥pen, velkommende, mangfold, inklusiv og sunn oppførsel i vÃ¥rt felleskap. + +## VÃ¥re Standarer + +Eksempler pÃ¥ oppførsel som hjelper ett positivt felleskap inkluderer: + +* Vise empati og vennlighet mot andre mennesker +* Være respektfull ovenfor ulike meninger, synspunkter og erfaringer +* Gi og ta konstruktiv kritikk i beste mening +* Akseptere ansvar og unskylde seg for de som er utsatt av vÃ¥re feil, + og lære av disse +* Fokusere pÃ¥ det som er best ikke bare for individer, men for felleskapet + +Eksempler pÃ¥ uakseptabel oppførsel inkluderer: + +* Bruk av seksualisert sprÃ¥k eller bilder, og seksual oppmerksomhet. +* Troll-ene, fornermende og nedsettende kommentarer, og personlig eller politiske angrep +* Offentlig eller privat trakassering +* Publisering av andres private informasjon, sÃ¥nn som bosteds- og epost-addresser, + uten deres godskjenning. +* Andre rettningslinjer som kan bli sett pÃ¥ som upassende i en profesjonell setting. + +## HÃ¥ndhevingsansvar + +Felleskapets ledere har ansvar for Ã¥ klarifisere og hÃ¥ndheve vÃ¥re standarer av +akseptert oppførsel og vill ta rimelige og rettferdige handliger som respons pÃ¥ +oppførsel de anser som upassende, truende, fornermende eller skadelig. + +Felleskapets ledere har retten og ansvaret til Ã¥ fjerne, redigere, eller avslÃ¥ +kommentarer, commits, kode, wiki endringer, issues, og andre birag som ikke +samsvarer med disse etiske rettningslinjene, og vill kommunisere grunner for +moderatorenes valg nÃ¥r passende. + +## Omfang + +Disse etiske rettningslinjene gjelder innenfor alle platformene til felleskapet, og +de gjelder ogsÃ¥ nÃ¥r ett individ representerer felleskapet pÃ¥ offentlige medier. +Eksempler pÃ¥ representasjon av vÃ¥rt felleskap inkluderer bruke av offisielle e-mail +addresser, publisering gjennom en offisiell sosial media bruker, eller oppførsel som en +utpekt representant pÃ¥ digitale og fysiske arrangsjemanger. + +## HÃ¥ndheving + +Hendelser av misbruk, trakasserende eller pÃ¥ noen mÃ¥te uakseptert oppførsel kann +bli raportert til felleskapets ledere med ansvar for hÃ¥ndheving pÃ¥ +[info@rustdesk.com](mailto:info@rustdesk.com). +All tilbakemelding vill bli sett gjennom og investigert rettferdig sÃ¥ fort som mulig. + +Alle felleskapets ledere er obligert til Ã¥ respektere privatlivet og sikkerhetet ovenfor +den som raporterer en hendelse. + +## HÃ¥ndhevings Guide + +Felleskapets ledere vill følge disse Rettningslinjene for sammfunspÃ¥virkning med +tanke pÃ¥ konsekvenser for en handling de anser i brudd med disse etiske rettningslinjene: + +### 1. Korreksjon + +**SammfunspÃ¥virkning**: Bruk av upassende sprÃ¥k eller annen oppførsel ansett som +uprofesjonelt eller uvelkommen i dette felleskapet. + +**Konsekvens**: En privat, skrevet advarsel fra en leder av felleskapet, som +klarifiserer grunnlaget til hvorfor denne oppførselen var upassende. En offentlig +unskyldning kan bli forespurt. + +### 2. Advarsel + +**SammfunspÃ¥virkning**: Ett brudd pÃ¥ en singulær hendelse eller en serie handlinger. + +**Konsekvens**: En advarsel med konsekvenser for kontinuerende oppførsel. Ingen +interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med +de som hÃ¥ndhever disse etiske rettningslinjene, er tillat for en spesifisert tidsperiode. +Dette inkluderer Ã¥ unngÃ¥ interaksjoner i felleskapets platformer, samt eksterne +kanaler, som f.eks sosial media. Brudd av disse vilkÃ¥rene kan føre til midlertidig +eller permanent bannlysning. + +### 3. Midlertidig Bannlysning + +**SammfunspÃ¥virkning**: Ett særiøst brudd pÃ¥ felleskapets standarer, inkludert +vedvarende upassende oppførsel. + +**Konsekvens**: En midlertidig bannlysning fra noen som helst interaksjon eller +offentlig kommunikasjon med felleskapet for en spesifisert tidsperiode. Ingen +interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med +de som hÃ¥ndhever disse etiske rettningslinjene, er tillat for denne perioden. +Brudd pÃ¥ disse vilkÃ¥rene kan føre til permanent bannlysning. + +### 4. Permanent Bannlysning + +**SammfunspÃ¥virkning**: Demonstasjon av mønster i brudd pÃ¥ felleskapets standarer, +inklusivt vedvarende upassende oppførsel, trakassering av ett individ, eller +aggresjon mot eller nedsettelse av grupper individer. + +**Konsekvens**: En permanent bannlysning fra alle offentlige interaksjoner i +felleskapet + +## Attribusjon + +Disse etiske rettningslinjene er adaptert fra [Contributor Covenant][homepage], +versjon 2.0, tilgjengelig ved +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +SammfunspÃ¥virknings guid inspirert av +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For svar til vanlige spørsmÃ¥l angÃ¥ende disse etiske rettningslinjene, se FAQ pÃ¥ +[https://www.contributor-covenant.org/faq][FAQ]. Oversettelse tilgjengelig +ved [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-PL.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-PL.md new file mode 100644 index 0000000..8aedf83 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-PL.md @@ -0,0 +1,133 @@ + +# Kod postÄ™powania Contributor Covenant Code of Conduct + +## Nasza przysiÄ™ga + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Nasze standardy + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[info@rustdesk.com](mailto:info@rustdesk.com). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-RO.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-RO.md new file mode 100644 index 0000000..6fe8564 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-RO.md @@ -0,0 +1,85 @@ +# Codul de Conduită al Contributorilor + +## Angajamentul Nostru + +Noi, ca membri, contribuitori È™i lideri, ne angajăm să facem ca participarea în comunitatea noastră să fie o experiență fără hărÈ›uire pentru toată lumea, indiferent de vârstă, dimensiunea corpului, dizabilități vizibile sau invizibile, etnie, caracteristici sexuale, identitate È™i exprimare de gen, nivel de experiență, educaÈ›ie, statut socio-economic, naÈ›ionalitate, aspect personal, rasă, religie sau identitate È™i orientare sexuală. + +Ne angajăm să acÈ›ionăm È™i să interacÈ›ionăm în moduri care contribuie la o comunitate deschisă, primitoare, diversă, incluzivă È™i sănătoasă. + +## Standardele Noastre + +Exemple de comportamente care contribuie la un mediu pozitiv pentru comunitatea noastră includ: + +* Demonstrarea empatiei È™i a bunătății față de ceilalÈ›i +* Respectarea opiniilor, punctelor de vedere È™i experienÈ›elor diferite +* Oferirea È™i acceptarea cu graÈ›ie a feedback-ului constructiv +* Asumarea responsabilității È™i cererea de scuze celor afectaÈ›i de greÈ™elile noastre È™i învățarea din experiență +* Concentrarea pe ceea ce este cel mai bun nu doar pentru noi ca indivizi, ci pentru întreaga comunitate + +Exemple de comportamente inacceptabile includ: + +* Utilizarea limbajului sau imaginilor sexualizate, precum È™i atenÈ›ia sau avansurile sexuale de orice fel +* Trollare, insulte sau comentarii denigratoare È™i atacuri personale sau politice +* HărÈ›uire publică sau privată +* Publicarea informaÈ›iilor private ale altora, cum ar fi adresa fizică sau de e-mail, fără permisiunea explicită +* Alte comportamente care ar putea fi considerate inadecvate într-un cadru profesional + +## Responsabilități de Aplicare + +Liderii comunității sunt responsabili pentru clarificarea È™i aplicarea standardelor noastre de comportament acceptabil È™i vor lua măsuri corective adecvate È™i echitabile ca răspuns la orice comportament pe care îl consideră inadecvat, amenințător, ofensator sau dăunător. + +Liderii comunității au dreptul È™i responsabilitatea de a elimina, edita sau respinge comentarii, commit-uri, cod, editări wiki, probleme È™i alte contribuÈ›ii care nu se aliniază acestui Cod de Conduită È™i vor comunica motivele pentru deciziile de moderare atunci când este cazul. + +## Domeniu de Aplicare + +Acest Cod de Conduită se aplică în toate spaÈ›iile comunității È™i se aplică È™i atunci când un individ reprezintă oficial comunitatea în spaÈ›ii publice. +Exemple de reprezentare a comunității includ utilizarea unei adrese de e-mail oficiale, postarea printr-un cont oficial de social media sau acÈ›ionarea ca reprezentant desemnat la un eveniment online sau offline. + +## Aplicare + +Cazurile de comportament abuziv, hărÈ›uitor sau altfel inacceptabil pot fi raportate liderilor comunității responsabili pentru aplicare la [info@rustdesk.com](mailto:info@rustdesk.com). +Toate plângerile vor fi revizuite È™i investigate prompt È™i corect. + +ToÈ›i liderii comunității sunt obligaÈ›i să respecte confidenÈ›ialitatea È™i securitatea persoanei care raportează orice incident. + +## Ghiduri de Aplicare + +Liderii comunității vor urma aceste Ghiduri privind Impactul Comunității pentru a stabili consecinÈ›ele pentru orice acÈ›iune pe care o consideră o încălcare a acestui Cod de Conduită: + +### 1. Corectare + +**Impact asupra comunității**: Utilizarea limbajului neadecvat sau alte comportamente considerate neprofesionale sau nedorite în comunitate. + +**Consecință**: O avertizare scrisă È™i privată din partea liderilor comunității, oferind claritate asupra naturii încălcării È™i o explicaÈ›ie despre motivul pentru care comportamentul a fost inadecvat. Poate fi cerută o scuză publică. + +### 2. Avertisment + +**Impact asupra comunității**: ÃŽncălcare printr-un incident singular sau o serie de acÈ›iuni. + +**Consecință**: Un avertisment cu consecinÈ›e pentru continuarea comportamentului. Nicio interacÈ›iune cu persoanele implicate, inclusiv interacÈ›iuni nesolicitate cu cei care aplică Codul de Conduită, pentru o perioadă specificată. Aceasta include evitarea interacÈ›iunilor în spaÈ›iile comunității, precum È™i pe canale externe, cum ar fi reÈ›elele sociale. ÃŽncălcarea acestor termeni poate duce la o suspendare temporară sau permanentă. + +### 3. Suspendare Temporară + +**Impact asupra comunității**: O încălcare serioasă a standardelor comunității, inclusiv comportament neadecvat susÈ›inut. + +**Consecință**: Suspendare temporară de la orice tip de interacÈ›iune sau comunicare publică cu comunitatea pentru o perioadă specificată. Nicio interacÈ›iune publică sau privată cu persoanele implicate, inclusiv interacÈ›iuni nesolicitate cu cei care aplică Codul de Conduită, nu este permisă în această perioadă. ÃŽncălcarea acestor termeni poate duce la o interdicÈ›ie permanentă. + +### 4. InterdicÈ›ie Permanentă + +**Impact asupra comunității**: Demonstrând un tipar de încălcare a standardelor comunității, inclusiv comportament neadecvat susÈ›inut, hărÈ›uire a unei persoane sau agresiune față de sau denigrare a unor grupuri de persoane. + +**Consecință**: InterdicÈ›ie permanentă de la orice tip de interacÈ›iune publică în cadrul comunității. + +## Atribuire + +Acest Cod de Conduită este adaptat din [Contributor Covenant][homepage], versiunea 2.0, disponibil la [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Ghidurile privind Impactul Comunității au fost inspirate de [scara de aplicare a codului de conduită Mozilla][Mozilla CoC]. + +Pentru răspunsuri la întrebări frecvente despre acest cod de conduită, vezi FAQ la [https://www.contributor-covenant.org/faq][FAQ]. Traduceri sunt disponibile la [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-RU.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-RU.md new file mode 100644 index 0000000..53f4ab8 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-RU.md @@ -0,0 +1,134 @@ + +# ÐšÐ¾Ð´ÐµÐºÑ Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ ÑƒÑ‡Ð°Ñтников и вкладчиков + +## Ðаше обещание + +Мы, как члены, вкладчики и лидеры, обÑзуемÑÑ Ñделать учаÑтие в нашем +ÑообщеÑтве Ñвободным от притеÑнений Ð´Ð»Ñ Ð²Ñех, незавиÑимо от возраÑта, +размера тела, видимой или невидимой инвалидноÑти, ÑтничеÑкой принадлежноÑти, половых характериÑтик, гендерной +идентичноÑти и ÑамовыражениÑ, ÑƒÑ€Ð¾Ð²Ð½Ñ Ð¾Ð¿Ñ‹Ñ‚Ð°, образованиÑ, Ñоциально-ÑкономичеÑкого ÑтатуÑа, +национальноÑти, внешнего вида, раÑÑ‹, религии или ÑекÑуальной идентичноÑти +и ориентации. + +Мы обÑзуемÑÑ Ð´ÐµÐ¹Ñтвовать и взаимодейÑтвовать таким образом, чтобы ÑпоÑобÑтвовать Ñозданию открытого, гоÑтеприимного, +разнообразного, инклюзивного и здорового ÑообщеÑтва. + +## Ðаши Стандарты + +Примеры поведениÑ, ÑпоÑобÑтвующего Ñозданию благоприÑтной Ñреды Ð´Ð»Ñ Ð½Ð°ÑˆÐµÐ³Ð¾ +ÑообщеÑтва, включают: + +* ДемонÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ ÑочувÑÑ‚Ð²Ð¸Ñ Ð¸ доброты по отношению к другим людÑм +* Уважительное отношение к различным мнениÑм, точкам Ð·Ñ€ÐµÐ½Ð¸Ñ Ð¸ опыту +* ПредоÑтавление и вежливое принÑтие конÑтруктивной обратной ÑвÑзи +* ПринÑтие ответÑтвенноÑти и Ð¸Ð·Ð²Ð¸Ð½ÐµÐ½Ð¸Ñ Ð¿ÐµÑ€ÐµÐ´ теми, кто поÑтрадал от наших ошибок, +а также извлечение уроков из накопленного опыта +* СоÑредоточение Ð²Ð½Ð¸Ð¼Ð°Ð½Ð¸Ñ Ð½Ð° том, что лучше не только Ð´Ð»Ñ Ð½Ð°Ñ ÐºÐ°Ðº отдельных людей, но и Ð´Ð»Ñ +вÑего ÑообщеÑтва в целом. + +Примеры неприемлемого Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð²ÐºÐ»ÑŽÑ‡Ð°ÑŽÑ‚: + +* ИÑпользование ÑекÑуализированных выражений или образов, а также ÑекÑуальное внимание или +Ð·Ð°Ð¸Ð³Ñ€Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð»ÑŽÐ±Ð¾Ð³Ð¾ рода +* Троллинг, оÑкорбительные или уничижительные комментарии, а также личные или политичеÑкие нападки +* Публичные или чаÑтные домогательÑтва +* ÐŸÑƒÐ±Ð»Ð¸ÐºÐ°Ñ†Ð¸Ñ Ð»Ð¸Ñ‡Ð½Ð¾Ð¹ информации других лиц, такой как физичеÑкий Ð°Ð´Ñ€ÐµÑ Ð¸Ð»Ð¸ Ð°Ð´Ñ€ÐµÑ Ñлектронной +почты, без их Ñвного Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ +* Другое поведение, которое можно обоÑнованно Ñчитать неумеÑтным в +профеÑÑиональной Ñреде + +## Правоприменительные обÑзанноÑти + +Лидеры ÑообщеÑтва неÑут ответÑтвенноÑть за разъÑÑнение и обеÑпечение ÑÐ¾Ð±Ð»ÑŽÐ´ÐµÐ½Ð¸Ñ Ð½Ð°ÑˆÐ¸Ñ… Ñтандартов +приемлемого Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¸ предпримут надлежащие и Ñправедливые корректирующие дейÑÑ‚Ð²Ð¸Ñ Ð² +ответ на любое поведение, которое они Ñочтут неумеÑтным, угрожающим, оÑкорбительным +или вредным. + +Лидеры ÑообщеÑтва имеют право и ответÑтвенноÑть удалÑть, редактировать или отклонÑть +комментарии, коммиты, код, вики-правки, проблемы и другие материалы, которые +не ÑоответÑтвуют наÑтоÑщему КодекÑу поведениÑ, и +при необходимоÑти Ñообщат причины принÑÑ‚Ð¸Ñ Ñ€ÐµÑˆÐµÐ½Ð¸Ð¹ о модерации. + +## Сфера дейÑÑ‚Ð²Ð¸Ñ + +Этот ÐšÐ¾Ð´ÐµÐºÑ Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¸Ð¼ÐµÐ½ÑетÑÑ Ð²Ð¾ вÑех общеÑтвенных меÑтах, а также применÑетÑÑ, когда +физичеÑкое лицо официально предÑтавлÑет ÑообщеÑтво в общеÑтвенных меÑтах. +Примеры предÑÑ‚Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð½Ð°ÑˆÐµÐ³Ð¾ ÑообщеÑтва включают иÑпользование официального адреÑа Ñлектронной почты, +размещение Ñообщений через официальную учетную запиÑÑŒ в Ñоциальных ÑетÑÑ… или выÑтупление в качеÑтве назначенного +предÑÑ‚Ð°Ð²Ð¸Ñ‚ÐµÐ»Ñ Ð½Ð° онлайн- или оффлайн-мероприÑтии. + +## Правоприменение + +О ÑлучаÑÑ… оÑкорбительного, домогательÑкого или иного неприемлемого Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¼Ð¾Ð¶Ð½Ð¾ +Ñообщать лидерам ÑообщеÑтва, ответÑтвенным за правоприменение в +[info@rustdesk.com ](mailto:info@rustdesk.com). +Ð’Ñе жалобы будут раÑÑмотрены и раÑÑледованы быÑтро и Ñправедливо. + +Ð’Ñе лидеры ÑообщеÑтва обÑзаны уважать чаÑтную жизнь и безопаÑноÑть +репортера о любом инциденте. + +## РуководÑщие принципы воздейÑÑ‚Ð²Ð¸Ñ + +Лидеры ÑообщеÑтва будут Ñледовать Ñтим руководÑщим принципам воздейÑÑ‚Ð²Ð¸Ñ Ð½Ð° ÑообщеÑтво при определении +поÑледÑтвий любого дейÑтвиÑ, которое они Ñочтут нарушением наÑтоÑщего КодекÑа поведениÑ: + +### 1. Правки + +**ВоздейÑтвие на ÑообщеÑтво**: ИÑпользование неподобающих выражений или другого поведениÑ, которое ÑчитаетÑÑ +непрофеÑÑиональным или нежелательным в ÑообщеÑтве. + +**ПоÑледÑтвие**: чаÑтное пиÑьменное предупреждение от лидеров ÑообщеÑтва, дающее +ÑÑноÑть в отношении характера Ð½Ð°Ñ€ÑƒÑˆÐµÐ½Ð¸Ñ Ð¸ объÑÑнение того, почему +поведение было неумеÑтным. Могут быть запрошены публичные извинениÑ. + + +### 2. Предупреждение + +**ВоздейÑтвие на ÑообщеÑтво**: нарушение в результате одного инцидента или Ñерии +дейÑтвий. + +**ПоÑледÑтвие**: Предупреждение Ñ Ð¿Ð¾ÑледÑтвиÑми Ð´Ð»Ñ Ð´Ð°Ð»ÑŒÐ½ÐµÐ¹ÑˆÐµÐ³Ð¾ поведениÑ. Ðикакого +взаимодейÑÑ‚Ð²Ð¸Ñ Ñ Ð²Ð¾Ð²Ð»ÐµÑ‡ÐµÐ½Ð½Ñ‹Ð¼Ð¸ лицами, Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ Ð½ÐµÐ¶ÐµÐ»Ð°Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ðµ взаимодейÑтвие Ñ +теми, кто обеÑпечивает Ñоблюдение КодекÑа поведениÑ, в течение определенного периода времени. Это +включает в ÑÐµÐ±Ñ Ð¸Ð·Ð±ÐµÐ³Ð°Ð½Ð¸Ðµ взаимодейÑÑ‚Ð²Ð¸Ñ Ð² общеÑтвенных проÑтранÑтвах, а также внешних каналов +, таких как Ñоциальные Ñети. Ðарушение Ñтих уÑловий может привеÑти к временному или +поÑтоÑнному запрету. + +### 3. Ð’Ñ€ÐµÐ¼ÐµÐ½Ð½Ð°Ñ Ð±Ð»Ð¾ÐºÐ¸Ñ€Ð¾Ð²ÐºÐ° + +**ВоздейÑтвие на ÑообщеÑтво**: Серьезное нарушение Ñтандартов ÑообщеÑтва, Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ +длительное неподобающее поведение. + +**ПоÑледÑтвие**: Временный запрет на любое взаимодейÑтвие или публичное +общение Ñ ÑообщеÑтвом в течение определенного периода времени. +Ð’ течение Ñтого периода не допуÑкаетÑÑ Ð½Ð¸ÐºÐ°ÐºÐ¾Ðµ публичное или чаÑтное взаимодейÑтвие Ñ Ð²Ð¾Ð²Ð»ÐµÑ‡ÐµÐ½Ð½Ñ‹Ð¼Ð¸ лицами, Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ Ð½ÐµÐ·Ð°Ð¿Ñ€Ð°ÑˆÐ¸Ð²Ð°ÐµÐ¼Ð¾Ðµ взаимодейÑтвие +Ñ Ñ‚ÐµÐ¼Ð¸, кто обеÑпечивает Ñоблюдение КодекÑа поведениÑ. +Ðарушение Ñтих уÑловий может привеÑти к поÑтоÑнному запрету. + +### 4. Блокировка навÑегда + +**ВоздейÑтвие на ÑообщеÑтво**: ДемонÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ð¼Ð¾Ð´ÐµÐ»Ð¸ Ð½Ð°Ñ€ÑƒÑˆÐµÐ½Ð¸Ñ +Ñтандартов ÑообщеÑтва, Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ Ð¿Ð¾ÑтоÑнное неподобающее поведение, преÑледование отдельного +лица или агреÑÑию по отношению к клаÑÑам людей или пренебрежительное отношение к ним. + +**ПоÑледÑтвие**: ПоÑтоÑнный запрет на любое публичное взаимодейÑтвие внутри +ÑообщеÑтва. + +## Определение + +ÐаÑтоÑщий ÐšÐ¾Ð´ÐµÐºÑ Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð°Ð´Ð°Ð¿Ñ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½ из [Ð¡Ð¾Ð³Ð»Ð°ÑˆÐµÐ½Ð¸Ñ Ð¾ вкладчиках][homepage], +верÑии 2.0, доÑтупной по ÑÑылке +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +РуководÑщие принципы воздейÑÑ‚Ð²Ð¸Ñ Ð½Ð° ÑообщеÑтво были вдохновлены +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +Ответы на раÑпроÑтраненные вопроÑÑ‹ об Ñтом кодекÑе Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ñм. в разделе ЧаÑто задаваемые вопроÑÑ‹ по адреÑу +[https://www.contributor-covenant.org/faq][FAQ]. Переводы доÑтупны +по адреÑу [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-TR.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-TR.md new file mode 100644 index 0000000..76088bd --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-TR.md @@ -0,0 +1,89 @@ +# Katkıda Bulunanların Davranış Kuralları + +## Taahhüdümüz + +Biz üyeler, katkıda bulunanlar ve liderler olarak, yaÅŸ, beden büyüklüğü, görünür veya görünmez engellilik, etnik köken, cinsiyet özellikleri, cinsiyet kimliÄŸi ve ifadesi, deneyim seviyesi, eÄŸitim, sosyo-ekonomik durum, milliyet, kiÅŸisel görünüm, ırk, din veya cinsel kimlik ve yönelim ayrımı gözetmeksizin herkes için topluluÄŸumuzdaki katılımı taciz içermeyen bir deneyim haline getirmeyi taahhüt ederiz. + +Açık, hoÅŸgörülü, çeÅŸitli, kapsayıcı ve saÄŸlıklı bir topluluÄŸa katkıda bulunacak ÅŸekillerde hareket etmeyi ve etkileÅŸimde bulunmayı taahhüt ederiz. + +## Standartlarımız + +TopluluÄŸumuz için olumlu bir ortam yaratmaya katkıda bulunan davranış örnekleri ÅŸunlardır: + +* DiÄŸer insanlara empati ve nezaket göstermek +* Farklı görüşlere, bakış açılarına ve deneyimlere saygılı olmak +* Yapıcı eleÅŸtiriyi vermek ve zarifçe kabul etmek +* Hatalarımızdan etkilenenlere sorumluluk kabul etmek, özür dilemek ve deneyimden öğrenmek +* Sadece bireyler olarak deÄŸil, aynı zamanda genel topluluk için en iyisi üzerine odaklanmak + +Kabul edilemez davranış örnekleri ÅŸunları içerir: + +* CinselleÅŸtirilmiÅŸ dil veya imgelerin kullanımı ve cinsel ilgi veya herhangi bir türdeki yaklaşımlar +* Trollük, aÅŸağılayıcı veya hakaret içeren yorumlar ve kiÅŸisel veya siyasi saldırılar +* Kamuoyu veya özel taciz +* BaÅŸkalarının fiziksel veya e-posta adresi gibi özel bilgilerini, açık izinleri olmadan yayınlamak +* Profesyonel bir ortamda makul bir ÅŸekilde uygunsuz kabul edilebilecek diÄŸer davranışlar + +## Uygulama Sorumlulukları + +Topluluk liderleri, kabul edilebilir davranış standartlarımızı açıklığa kavuÅŸturmak ve uygulamakla sorumludur ve uygunsuz, tehditkar, saldırgan veya zarar verici herhangi bir davranışa yanıt olarak uygun ve adil düzeltici önlemler alacaklardır. + +Topluluk liderleri, bu Davranış Kurallarına uyumlu olmayan yorumları, taahhütlerini veya kodu, wiki düzenlemelerini, sorunları ve diÄŸer katkıları kaldırma, düzenleme veya reddetme hakkına sahiptir. Denetim kararlarının nedenlerini uygun olduÄŸunda ileteceklerdir. + +## Kapsam + +Bu Davranış Kuralları, tüm topluluk alanlarında geçerlidir ve aynı zamanda birey resmi olarak topluluÄŸu halka açık alanlarda temsil ettiÄŸinde de geçerlidir. TopluluÄŸumuzu temsil etme örnekleri, resmi bir e-posta adresi kullanmak, resmi bir sosyal medya hesabı üzerinden gönderi yapmak veya çevrimiçi veya çevrimdışı bir etkinlikte atanmış bir temsilci olarak hareket etmeyi içerir. + +## Uygulama + +Taciz edici, rahatsız edici veya baÅŸka türlü kabul edilemez davranış örnekleri, [info@rustdesk.com](mailto:info@rustdesk.com) adresindeki uygulama sorumlularına bildirilebilir. Tüm ÅŸikayetler hızlı ve adil bir ÅŸekilde incelenecek ve araÅŸtırılacaktır. + +Tüm topluluk liderleri, olayın raporlayıcısının gizliliÄŸine ve güvenliÄŸine saygı gösterme yükümlülüğündedir. + +## Uygulama Kılavuzları + +Topluluk liderleri, bu Davranış Kurallarını ihlal olarak deÄŸerlendirdikleri herhangi bir eylem için bu Topluluk Etkisi Kılavuzlarını izleyeceklerdir: + +### 1. Düzeltme + +**Topluluk Etkisi**: Topluluk içinde profesyonel veya hoÅŸgörülü olmayan uygun olmayan dil veya diÄŸer davranışların kullanımı. + +**Sonuç**: Topluluk liderlerinden özel ve yazılı bir uyarı almak, ihlalin niteliÄŸi ve davranışın nedeninin açıklığa kavuÅŸturulması. Bir kamu özrü istenebilir. + +### 2. Uyarı + +**Topluluk Etkisi**: Tek bir olay veya dizi aracılığıyla bir ihlal. + +**Sonuç**: Devam eden davranış için sonuçları olan bir uyarı. Topluluk liderleri de dahil olmak üzere ihlalle ilgili kiÅŸilerle etkileÅŸim, belirli bir süre boyunca önerilmez. Bu, topluluk alanlarında ve sosyal medya gibi harici kanallarda etkileÅŸimleri içerir. Bu koÅŸulları ihlal etmek geçici veya kalıcı bir yasaÄŸa yol açabilir. + +### 3. Geçici Yasak + +**Topluluk Etkisi**: Sürekli uygunsuz davranış da dahil olmak üzere topluluk standartlarının ciddi bir ihlali. + +**Sonuç**: Belirli bir süre için toplulukla herhangi bir türdeki etkileÅŸim veya halka açık iletiÅŸimden geçici bir yasak. Bu dönem boyunca, toplul + +ukla veya uygulama kurallarını uygulayanlarla her türlü kamuoyu veya özel etkileÅŸim izin verilmez. Bu koÅŸulları ihlal etmek geçici veya kalıcı bir yasaÄŸa yol açabilir. + +### 4. Kalıcı Yasak + +**Topluluk Etkisi**: Topluluk standartlarının ihlalinde sürekli bir desen sergilemek, bireye sürekli olarak uygun olmayan davranışlarda bulunmak, bir bireye tacizde bulunmak veya birey sınıflarına karşı saldırganlık veya aÅŸağılama yapmak. + +**Sonuç**: Topluluk içinde her türlü halka açık etkileÅŸimden kalıcı bir yasak. + +## Atıf + +Bu Davranış Kuralları, [Contributor Covenant][anasayfa], 2.0 sürümünden uyarlanmıştır ve +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0] adresinde bulunmaktadır. + +Topluluk Etkisi Kılavuzları, +[Mozilla'nın davranış kuralları uygulama merdiveni][Mozilla DK] tarafından ilham alınarak oluÅŸturulmuÅŸtur. + +Bu davranış kuralları hakkında yaygın soruların cevapları için, SSS'ye göz atın: +[https://www.contributor-covenant.org/faq][SSS]. Çeviriler, +[https://www.contributor-covenant.org/translations][çeviriler] adresinde bulunabilir. + +[anasayfa]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla DK]: https://github.com/mozilla/diversity +[SSS]: https://www.contributor-covenant.org/faq +[çeviriler]: https://www.contributor-covenant.org/translations diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-ZH.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-ZH.md new file mode 100644 index 0000000..0877ab2 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT-ZH.md @@ -0,0 +1,87 @@ + +# 贡献者公约行为准则 + +## 我们的承诺 + +身为社区æˆå‘˜ã€è´¡çŒ®è€…和领袖,我们承诺使社区å‚与者ä¸å—骚扰,无论其年龄ã€ä½“åž‹ã€å¯è§æˆ–ä¸å¯è§çš„ç¼ºé™·ã€æ—è£”ã€æ€§å¾ã€æ€§åˆ«è®¤åŒå’Œè¡¨è¾¾ã€ç»éªŒæ°´å¹³ã€æ•™è‚²ç¨‹åº¦ã€ç¤¾ä¼šä¸Žç»æµŽåœ°ä½ã€å›½ç±ã€ç›¸è²Œã€ç§æ—ã€ç§å§“ã€è‚¤è‰²ã€å®—æ•™ä¿¡ä»°ã€æ€§å€¾å‘或性å–å‘如何。 + +我们承诺以有助于建立开放ã€å‹å–„ã€å¤šæ ·åŒ–ã€åŒ…容ã€å¥åº·ç¤¾åŒºçš„æ–¹å¼è¡Œäº‹å’Œäº’动。 + +## 我们的标准 + +有助于为我们的社区创造积æžçŽ¯å¢ƒçš„è¡Œä¸ºä¾‹å­åŒ…括但ä¸é™äºŽï¼š + +* è¡¨çŽ°å‡ºå¯¹ä»–äººçš„åŒæƒ…å’Œå–„æ„ +* å°Šé‡ä¸åŒçš„主张ã€è§‚ç‚¹å’Œæ„Ÿå— +* æå‡ºå’Œå¤§æ–¹æŽ¥å—建设性æ„è§ +* 承担责任并å‘å—æˆ‘们错误影å“çš„äººé“æ­‰ +* 注é‡ç¤¾åŒºå…±åŒè¯‰æ±‚,而éžä¸ªäººå¾—失 + +ä¸å½“行为例å­åŒ…括: + +* 使用情色化的语言或图åƒï¼ŒåŠæ€§å¼•诱或挑逗 +* 嘲弄ã€ä¾®è¾±æˆ–è¯‹æ¯æ€§è¯„论,以åŠäººèº«æˆ–政治攻击 +* 公开或ç§ä¸‹çš„骚扰行为 +* 未ç»ä»–人明确许å¯ï¼Œå…¬å¸ƒä»–人的ç§äººä¿¡æ¯ï¼Œå¦‚ç‰©ç†æˆ–电å­é‚®ä»¶åœ°å€ +* 其他有ç†ç”±è®¤å®šä¸ºè¿åèŒä¸šæ“守的ä¸å½“行为 + +## 责任和æƒåŠ› + +社区领袖有责任解释和è½å®žæˆ‘们所认å¯çš„行为准则,并妥善公正地对他们认为ä¸å½“ã€å¨èƒã€å†’犯或有害的任何行为采å–纠正措施。 + +社区领导有æƒåŠ›å’Œè´£ä»»åˆ é™¤ã€ç¼–è¾‘æˆ–æ‹’ç»æˆ–æ‹’ç»ä¸Žæœ¬è¡Œä¸ºå‡†åˆ™ä¸ç›¸ç¬¦çš„评论(commentï¼‰ã€æäº¤ï¼ˆcommits)ã€ä»£ç ã€ç»´åŸºï¼ˆwiki)编辑ã€è®®é¢˜ï¼ˆissuesï¼‰æˆ–å…¶ä»–è´¡çŒ®ï¼Œå¹¶åœ¨é€‚å½“æ—¶æœºçŸ¥é‡‡å–æŽªæ–½çš„ç†ç”±ã€‚ + +## 适用范围 + +本行为准则适用于所有社区场åˆï¼Œä¹Ÿé€‚用于在公共场所代表社区时的个人。 + +代表社区的情形包括使用官方电å­é‚®ä»¶åœ°å€ã€é€šè¿‡å®˜æ–¹ç¤¾äº¤åª’体叿ˆ·å‘帖或在线上或线下活动中担任指定代表。 + +## ç›‘ç£ + +辱骂ã€éªšæ‰°æˆ–å…¶ä»–ä¸å¯æŽ¥å—的行为å¯é€šè¿‡[info@rustdesk.com](mailto:info@rustdesk.com)å‘负责监ç£çš„社区领袖报告。 æ‰€æœ‰æŠ•è¯‰éƒ½å°†å¾—åˆ°åŠæ—¶å’Œå…¬å¹³çš„审查和调查。 + +所有社区领袖都有义务尊é‡ä»»ä½•事件报告者的éšç§å’Œå®‰å…¨ã€‚ + +## å¤„ç†æ–¹é’ˆ + +社区领袖将éµå¾ªä¸‹åˆ—ç¤¾åŒºå¤„ç†æ–¹é’ˆæ¥æ˜Žç¡®ä»–们所认定è¿åæœ¬è¡Œä¸ºå‡†åˆ™çš„è¡Œä¸ºçš„å¤„ç†æ–¹å¼ï¼š + +### 1. 纠正 + +**社区影å“**: ä½¿ç”¨ä¸æ°å½“的语言或其他在社区中被认定为ä¸ç¬¦åˆèŒä¸šé“德或ä¸å—欢迎的行为。 + +**å¤„ç†æ„è§**: 由社区领袖å‘出éžå…¬å¼€çš„书é¢è­¦å‘Šï¼Œæ˜Žç¡®è¯´æ˜Žè¿è§„行为的性质,并解释举止如何ä¸å¦¥ã€‚æˆ–å°†è¦æ±‚公开铿­‰ã€‚ + +### 2. 警告 + +**社区影å“**: å•个或一系列è¿è§„行为。 + +**å¤„ç†æ„è§**: 警告并对连续性行为进行处ç†ã€‚在指定时间内,ä¸å¾—与相关人员互动,包括主动与行为准则执行者互动。这包括é¿å…在社区场所和外部渠é“中的互动。è¿åè¿™äº›æ¡æ¬¾å¯èƒ½ä¼šå¯¼è‡´ä¸´æ—¶æˆ–永久å°ç¦ã€‚ + +### 3. 临时å°ç¦ + +**社区影å“**: 严é‡è¿å社区准则,包括æŒç»­çš„ä¸å½“行为。 + +**å¤„ç†æ„è§**: åœ¨æŒ‡å®šæ—¶é—´å†…ï¼Œæš‚æ—¶ç¦æ­¢ä¸Žç¤¾åŒºè¿›è¡Œä»»ä½•å½¢å¼çš„互动或公开交æµã€‚在此期间,ä¸å¾—与相关人员进行公开或ç§ä¸‹äº’动,包括主动与行为准则执行者互动。è¿åè¿™äº›æ¡æ¬¾å¯èƒ½ä¼šå¯¼è‡´æ°¸ä¹…å°ç¦ã€‚ + +### 4. 永久å°ç¦ + +**社区影å“**: 行为模å¼è¡¨çŽ°å‡ºè¿å社区准则,包括æŒç»­çš„ä¸å½“行为ã€éªšæ‰°ä¸ªäººæˆ–攻击或贬低æŸä¸ªç±»åˆ«çš„个体。 + +**å¤„ç†æ„è§**: æ°¸ä¹…ç¦æ­¢åœ¨ç¤¾åŒºå†…进行任何形å¼çš„公开互动。 + +## å‚è§ + +本行为准则改编自[å‚与者公约][homepage]2.0 版, å‚è§ +[https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html][v2.0]. + +指导方针借鉴自[Mozilla纪检分级][Mozilla CoC]. + +有关本行为准则的常è§é—®é¢˜çš„答案,å‚è§ [https://www.contributor-covenant.org/faq][FAQ]。 其他语言翻译å‚è§[https://www.contributor-covenant.org/translations][translations]。 + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/zh-cn/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT.md b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e5db8ed --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[info@rustdesk.com](mailto:info@rustdesk.com). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-DE.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-DE.md new file mode 100644 index 0000000..b45c23d --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-DE.md @@ -0,0 +1,50 @@ +# Beiträge zu RustDesk + +RustDesk begrüßt Beiträge von jedem. Hier sind die Richtlinien, wenn Sie uns +helfen möchten: + +## Beiträge + +Beiträge zu RustDesk oder seinen Abhängigkeiten sollten in Form von Pull +Requests auf GitHub erfolgen. Jeder Pull Request wird von einem Hauptakteur +(jemand mit der Erlaubnis, Korrekturen einzubringen) geprüft und entweder in den +Hauptbaum eingefügt oder Feedback für notwendige Änderungen gegeben. Alle +Beiträge sollten diesem Format folgen, auch die von Hauptakteuren. + +Wenn Sie an einem Problem arbeiten möchten, melden Sie es bitte zuerst an, indem +Sie auf GitHub erklären, dass Sie daran arbeiten möchten. Damit soll verhindert +werden, dass Beiträge zum gleichen Thema doppelt bearbeitet werden. + +## Checkliste für Pull Requests + +- Verzweigen Sie sich vom Master-Branch und, falls nötig, wechseln Sie zum + aktuellen Master-Branch, bevor Sie Ihren Pull Request einreichen. Wenn das + Zusammenführen mit dem Master nicht reibungslos funktioniert, werden Sie + möglicherweise aufgefordert, Ihre Änderungen zu überarbeiten. + +- Commits sollten so klein wie möglich sein und gleichzeitig sicherstellen, dass + jeder Commit unabhängig voneinander korrekt ist (d. h., jeder Commit sollte + sich übersetzen lassen und Tests bestehen). + +- Commits sollten von einem "Herkunftszertifikat für Entwickler" + (https://developercertificate.org) begleitet werden, das besagt, dass Sie (und + ggf. Ihr Arbeitgeber) mit den Bedingungen der [Projektlizenz](../LICENCE) + einverstanden sind. In Git ist dies die Option `-s` für `git commit`. + +- Wenn Ihr Patch nicht begutachtet wird oder Sie eine bestimmte Person zur + Begutachtung benötigen, können Sie einem Gutachter mit @ antworten und um eine + Begutachtung des Pull Requests oder einen Kommentar bitten. Sie können auch + per [E-Mail](mailto:info@rustdesk.com) um eine Begutachtung bitten. + +- Fügen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue + Funktion beziehen. + +Spezifische Git-Anweisungen finden Sie im [GitHub-Workflow](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Verhalten + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Kommunikation + +RustDesk-Mitarbeiter arbeiten häufig im [Discord](https://discord.gg/nDceKgxnkV). diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-ID.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-ID.md new file mode 100644 index 0000000..cdff6c0 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-ID.md @@ -0,0 +1,31 @@ +# Berkontribusi dalam pengembangan RustDesk + +RustDesk mengajak semua orang untuk ikut berkontribusi. Berikut ini adalah panduan jika kamu sedang mempertimbangkan untuk memberikan bantuan kepada kami: + +## Kontirbusi + +Untuk melakukan kontribusi pada RustDesk atau dependensinya, sebaiknya dilakukan dalam bentuk pull request di GitHub. Setiap permintaan pull request akan ditinjau oleh kontributor utama atau seseorang yang memiliki wewenang untuk menggabungkan perubahan kode, baik yang sudah dimasukkan ke dalam struktur utama ataupun memberikan umpan balik untuk perubahan yang akan diperlukan. Setiap kontribusi harus sesuai dengan format ini, juga termasuk yang berasal dari kontributor utama. + +Apabila kamu ingin mengatasi sebuah masalah yang sudah ada di daftar issue, harap klaim terlebih dahulu dengan memberikan komentar pada GitHub issue yang ingin kamu kerjakan. Hal ini dilakukan untuk mencegah terjadinya duplikasi dari kontributor pada daftar issue yang sama. + +## Pemeriksaan Pull Request + +- Branch yang menjadi acuan adalah branch master dari repositori utama dan, jika diperlukan, lakukan rebase ke branch master yang terbaru sebelum kamu mengirim pull request. Apabila terdapat masalah kita melakukan proses merge ke branch master kemungkinan kamu akan diminta untuk melakukan rebase pada perubahan yang sudah dibuat. + +- Sebaiknya buatlah commit seminimal mungkin, sambil memastikan bahwa setiap commit yang dibuat sudah benar (contohnya, setiap commit harus bisa di kompilasi dan berhasil melewati tahap test). + +- Setiap commit harus disertai dengan tanda tangan Sertifikat Asal Pengembang (Developer Certificate of Origin) (), yang mengindikasikan bahwa kamu (and your employer if applicable) bersedia untuk patuh terhadap persyaratan dari [lisensi projek](../LICENCE). Di git bash, ini adalah opsi parameter `-s` pada `git commit` + +- Jika perubahan yang kamu buat tidak mendapat tinjauan atau kamu membutuhkan orang tertentu untuk meninjaunya, kamu bisa @-reply seorang reviewer meminta peninjauan dalam permintaan pull request atau komentar, atau kamu bisa meminta tinjauan melalui [email](mailto:info@rustdesk.com). + +- Sertakan test yang relevan terhadap bug atau fitur baru yang sudah dikerjakan. + +Untuk instruksi Git yang lebih lanjut, cek disini [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Tindakan + + + +## Komunikasi + +Kontributor RustDesk sering berkunjung ke [Discord](https://discord.gg/nDceKgxnkV). diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-IT.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-IT.md new file mode 100644 index 0000000..a3a5fd2 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-IT.md @@ -0,0 +1,37 @@ +# Contribuzione a RustDesk + +RustDesk accoglie con favore il contributo di tutti. +Ecco le linee guida se stai pensando di aiutarci. + +## Contribuzione + +I contributi a RustDesk o alle sue dipendenze dovrebbero essere forniti sotto forma di richieste pull GitHub. +Ogni richiesta pull verrà esaminata da un collaboratore principale (qualcuno con il permesso di applicare) ed è abilitato all'uso dell'albero principale o dare un feedback per le modifiche che sarebbero necessarie. +Tutti i contributi dovrebbero seguire questo formato, anche quelli dei contributori principali. + +Se desideri lavorare su un problema, rivendicalo prima commentando +il problema di GitHub su cui vuoi lavorare. +Questo per evitare duplicati sforzi dei contributori sullo stesso problema. + +## Elenco di controllo delle richieste pull + +- Branch del master branch e, se necessario, rebase al master attuale branch prima di inviare la richiesta pull. + Se l'unione non è in mod pulito con il master ti potrebbe essere chiesto di effettuare il rebase delle modifiche. + +- Le modifiche dovrebbero essere le più piccole possibile, assicurando al tempo stesso che ogni modifica sia corretta in modo indipendente (ovvero, ogni modifica dovrebbe essere compilabile e superare i test). + +- Le modifiche devono essere accompagnati da un certificato di origine per sviluppatori firmato (http://developercertificate.org), che indica che tu (e il tuo datore di lavoro se applicabile) accetti di essere vincolato dai termini della [licenza progetto](../LICENCE). In git, questa è l'opzione `-s` di `git commit` + +- Se la tua patch non viene esaminata o hai bisogno che una persona specifica la esamini, puoi @-rispondere ad un revisore chiedendo una revisione nella richiesta pull o un commento, oppure puoi chiedere una revisione tramite [email](mailto:info@rustdesk.com). + +- Aggiungi test relativi al bug corretto o alla nuova funzionalità. + +Per istruzioni specifiche su git, vedi [Workflow GitHub - 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Condotta + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-IT.md + +## Comunicazioni + +I contributori di RustDesk frequentano [Discord](https://discord.gg/nDceKgxnkV). diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-JP.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-JP.md new file mode 100644 index 0000000..03a1853 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-JP.md @@ -0,0 +1,41 @@ +# RustDesk ã¸ã®ã‚³ãƒ³ãƒˆãƒªãƒ“ュート + +RustDesk ã¯çš†ã•ã‚“ã‹ã‚‰ã®ã‚³ãƒ³ãƒˆãƒªãƒ“ュートを歓迎ã—ã¾ã™ã€‚ã”å”力ã„ãŸã ã‘ã‚‹æ–¹ã®ã‚¬ã‚¤ãƒ‰ãƒ©ã‚¤ãƒ³ã¯ +以下ã®é€šã‚Šã§ã™: + +## コントリビューション + +RustDesk ã¾ãŸã¯ãã®ä¾å­˜é–¢ä¿‚ã¸ã®ã‚³ãƒ³ãƒˆãƒªãƒ“ュートã¯ã€GitHub ã®ãƒ—ルリクエストã®å½¢ã§è¡Œã£ã¦ãã ã•ã„。 +ãれãžã‚Œã®ãƒ—ルリクエストã¯ã€ã‚³ã‚¢ã‚³ãƒ³ãƒˆãƒªãƒ“ューター(パッãƒã®é©ç”¨ã‚’許å¯ã•れã¦ã„る人)ã«ã‚ˆã£ã¦ãƒ¬ãƒ“ューã•れ〠+メインツリーã«é©ç”¨ã•れるã‹ã€å¿…è¦ãªå¤‰æ›´ã«ã¤ã„ã¦ã®ãƒ•ィードãƒãƒƒã‚¯ãŒä¸Žãˆã‚‰ã‚Œã¾ã™ã€‚ +コアコントリビューターã‹ã‚‰ã®ã‚‚ã®ã§ã‚ã£ã¦ã‚‚ã€ã™ã¹ã¦ã®ã‚³ãƒ³ãƒˆãƒªãƒ“ューターã¯ã“ã®ãƒ•ォーマットã«å¾“ã†ã¹ãã§ã™ã€‚ + +ã‚ã‚‹ issue ã«å–り組ã¿ãŸã„å ´åˆã¯ã€GitHub ã® issue ã«ã‚³ãƒ¡ãƒ³ãƒˆã™ã‚‹ã“ã¨ã§ã€ã¾ãšãã®å¯¾å¿œã‚’主張ã—ã¦ãã ã•ã„。 +ã“れã¯ã€åŒã˜ issue ã«å¯¾ã™ã‚‹ã‚³ãƒ³ãƒˆãƒªãƒ“ューターã®é‡è¤‡ä½œæ¥­ã‚’防ããŸã‚ã§ã™ã€‚ + +## プルリクエストã®ãƒã‚§ãƒƒã‚¯ãƒªã‚¹ãƒˆ + +- master ブランãƒã‹ã‚‰ãƒ–ランãƒã—ã€å¿…è¦ã§ã‚れã°ãƒ—ルリクエストをæå‡ºã™ã‚‹å‰ã«ç¾åœ¨ã® master ブランãƒã«ãƒªãƒ™ãƒ¼ã‚¹ã—ã¦ãã ã•ã„。 + master ã¨æ­£ã—ãマージã§ããªã„å ´åˆã€å¤‰æ›´ã‚’リベースã™ã‚‹ã‚ˆã†æ±‚ã‚られるå¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚ + +- コミットã¯ã€å„コミットãŒç‹¬ç«‹ã—ã¦æ­£ã—ã„(ã™ãªã‚ã¡ã€å„コミットãŒã‚³ãƒ³ãƒ‘イルã•れã€ãƒ†ã‚¹ãƒˆã«åˆæ ¼ã™ã‚‹ï¼‰ã“ã¨ã‚’ä¿è¨¼ã—ãªãŒã‚‰ã€ + å¯èƒ½ãªé™ã‚Šå°ã•ãã™ã¹ãã§ã™ã€‚ + +- コミットã«ã¯ã€Developer Certificate of Origin (http://developercertificate.org) ã® sign-off ã‚’æ·»ãˆã¦ãã ã•ã„。 + ã“れã¯ã€ã‚ãªãŸï¼ˆãŠã‚ˆã³è©²å½“ã™ã‚‹å ´åˆã¯ã‚ãªãŸã®é›‡ç”¨ä¸»ï¼‰ãŒ [プロジェクトã®ãƒ©ã‚¤ã‚»ãƒ³ã‚¹](../LICENCE) ã®æ¡é …ã«æ‹˜æŸã•れるã“ã¨ã« + åŒæ„ã—ã¦ã„ã‚‹ã“ã¨ã‚’示ã™ã‚‚ã®ã§ã™ã€‚git ã§ã¯ã€ã“れ㯠`git commit` ã® `-s` オプションを使ã„ã¾ã™ã€‚ + +- ã‚‚ã—ã‚ãªãŸã®ãƒ‘ッãƒãŒãƒ¬ãƒ“ューã•れãªã‹ã£ãŸã‚Šã€ç‰¹å®šã®äººã«ãƒ¬ãƒ“ューã—ã¦ã‚‚らã†å¿…è¦ãŒã‚ã‚‹å ´åˆã€ + プルリクエストやコメントã§ãƒ¬ãƒ“ューをä¾é ¼ã™ã‚‹ãƒ¬ãƒ“ュアーã«@返信ã—ãŸã‚Šã€[email](mailto:info@rustdesk.com) ã§ãƒ¬ãƒ“ューをä¾é ¼ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + +- 修正ã—ãŸãƒã‚°ã‚„新機能ã«é–¢é€£ã™ã‚‹ãƒ†ã‚¹ãƒˆã‚’追加ã™ã‚‹ã€‚ + +具体的ãªgitã®æ‰‹é †ã«ã¤ã„ã¦ã¯ã€[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)ã‚’å‚ç…§ã—ã¦ãã ã•ã„。 + +## 行動è¦ç¯„ + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## コミュニケーション + +RustDesk ã®ã‚³ãƒ³ãƒˆãƒªãƒ“ューターã¯ã€[Discord](https://discord.gg/nDceKgxnkV) を良ã使ã£ã¦ã„ã¾ã™ã€‚ diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-KR.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-KR.md new file mode 100644 index 0000000..5e43264 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-KR.md @@ -0,0 +1,46 @@ +# RustDesk 기여하기 + +RustDesk는 모든 ë¶„ë“¤ì˜ ì°¸ì—¬ë¥¼ 환ì˜í•©ë‹ˆë‹¤. ì €í¬ë¥¼ ë„와주실 ìƒê°ì´ 있으시다면 + ë‹¤ìŒ ì§€ì¹¨ì„ ë”°ë¥´ì„¸ìš”: + +## 기여 + +RustDesk ë˜ëŠ” ê·¸ 종ì†ì„±ì— 대한 기여는 GitHub í’€ 리퀘스트 형태로 +ì´ë£¨ì–´ì ¸ì•¼ 합니다. ê° í’€ 리퀘스트는 핵심 ê¸°ì—¬ìž (패치 ì ìš© ê¶Œí•œì´ +있는 사람)ê°€ 검토하여 ë©”ì¸ íŠ¸ë¦¬ì— ì¶”ê°€í•˜ê±°ë‚˜ 필요한 변경 ì‚¬í•­ì— +대한 í”¼ë“œë°±ì„ ì œê³µí•©ë‹ˆë‹¤. 핵심 기여ìžì˜ 기여를 í¬í•¨í•˜ì—¬ 모든 기여는 +ì´ í˜•ì‹ì„ ë”°ë¼ì•¼ 합니다. + +ì´ìŠˆì— ëŒ€í•´ 작업하고 싶으시면 먼저 해당 ì´ìŠˆì— ëŒ€í•´ 작업하고 싶다는 +ëŒ“ê¸€ì„ ë‹¬ì•„ 해당 ì´ìŠˆë¥¼ 요청하세요. ì´ëŠ” ë™ì¼í•œ ì´ìŠˆì— ëŒ€í•œ 기여ìžì˜ +ì¤‘ë³µëœ ë…¸ë ¥ì„ ë°©ì§€í•˜ê¸° 위한 것입니다. + +## í’€ 리퀘스트 ì²´í¬ë¦¬ìŠ¤íŠ¸ + +- Master 브랜치ì—서 브랜치를 만들고, 필요한 경우 í’€ 리퀘스트를 제출하기 + ì „ì— í˜„ìž¬ 마스터 브랜치로 리베ì´ìŠ¤í•˜ì„¸ìš”. 마스터 브랜치와 ê¹”ë”하게 + 병합ë˜ì§€ 않으면 변경 ì‚¬í•­ì„ ë¦¬ë² ì´ìŠ¤í•˜ë¼ëŠ” ìš”ì²­ì„ ë°›ì„ ìˆ˜ 있습니다. + +- ì»¤ë°‹ì€ ê°€ëŠ¥í•œ 한 작아야 하지만, ê° ì»¤ë°‹ì´ ë…립ì ìœ¼ë¡œ 올바른지 í™•ì¸ + 해야 합니다 (즉, ê° ì»¤ë°‹ì€ ì»´íŒŒì¼ë˜ì–´ 테스트를 통과해야 함). + +- 커밋ì—는 ê°œë°œìž ì¶œì²˜ ì¦ëª…서 (http://developercertificate.org) + ì„œëª…ì´ ì²¨ë¶€ë˜ì–´ì•¼ 하며, ì´ëŠ” 귀하 (ë° í•´ë‹¹ë˜ëŠ” 경우 고용주)ê°€ + [프로ì íЏ ë¼ì´ì„ ìФ](../LICENCE). ì¡°ê±´ì— êµ¬ì†ë˜ëŠ” ë° ë™ì˜í•œë‹¤ëŠ” ê²ƒì„ ë‚˜íƒ€ëƒ…ë‹ˆë‹¤. + gitì—서는 `git commit`ì— `-s` 옵션입니다 + +- 패치가 검토ë˜ì§€ 않거나 특정ì¸ì´ 검토해야 하는 경우, í’€ 리퀘스트나 + 댓글ì—서 검토ìžì—게 @-ë‹µê¸€ì„ ë³´ë‚´ 검토를 요청하거나 + [ì´ë©”ì¼](mailto:info@rustdesk.com)ì„ í†µí•´ 검토를 요청할 수 있습니다. + +- ìˆ˜ì •ëœ ë²„ê·¸ ë˜ëŠ” 새 기능과 ê´€ë ¨ëœ í…ŒìŠ¤íŠ¸ë¥¼ 추가합니다. + +구체ì ì¸ git 지침ì€, [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)ì„ ì°¸ì¡°í•˜ì„¸ìš”. + +## í–‰ë™ ê°•ë ¹ + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## 커뮤니케ì´ì…˜ + +RustDesk 기여ìžë“¤ì€ [Discord](https://discord.gg/nDceKgxnkV)ì—서 활ë™í•˜ê³  있습니다. diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-NL.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-NL.md new file mode 100644 index 0000000..a39e8ce --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-NL.md @@ -0,0 +1,50 @@ +# Bijdragen aan RustDesk + +RustDesk verwelkomt bijdragen van iedereen. Hier zijn de richtlijnen als u denkt +ons te willen helpen: + +## Bijdragen + +Bijdragen aan RustDesk of haar afhankelijkheden moeten worden gedaan in de +vorm van GitHub pull verzoeken. Elk pull verzoek zal worden beoordeeld door +een core bijdrager (iemand met toestemming om patches te plaatsen) en ofwel +worden geplaatst in de hoofd structuur of feedback krijgen voor veranderingen +die nodig zouden zijn. Alle bijdragen zouden dit formaat moeten volgen, +zelfs die van kernmedewerkers. + +Als je aan een onderwerp wilt werken, eis het dan eerst op door commentaar +te geven op het GitHub onderwerp dat je eraan wilt werken. Dit is om dubbele +inspanningen van medewerkers aan hetzelfde issue te voorkomen. + +## Checklist Pull Aanvragen + +- Maak een vertakking vanaf de master tak en, indien nodig, veranker naar de + huidige master tak voordat je je pull verzoek indient. Als je het niet netjes + samenvoegt met master kan je gevraagd worden om je wijzigingen + opnieuw op te bouwen. + +- Toezeggingen moeten zo klein mogelijk zijn, terwijl er voor gezorgd moet + worden dat elke toezegging onafhankelijk correct is (dat wil zeggen, elke + toezegging moet compileren en testen doorstaan). + +- Toezeggingen moeten vergezeld gaan van een Certificaat van Oorsprong + van de Ontwikkelaar (http://developercertificate.org) ondertekening, die aangeeft + dat u (en uw werkgever indien van toepassing) akkoord gaat met de + voorwaarden van het [project licentie](../LICENCE). + In git is dit de `-s` optie van `git commit` + +- Als je patch niet beoordeeld wordt of je hebt een specifiek persoon nodig om hem + te beoordelen kunt u @-reply een reviewer vragen in het pull verzoek of een + commentaar, of je kunt om een review vragen via [email](mailto:info@rustdesk.com). + +- Tests toevoegen die relevant zijn voor de gerepareerde bug of de nieuwe functie. + +Voor specifieke git instructies, zie [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Gedrag + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Communicatie + +RustDesk medewerkers bezoeken frequent [Discord](https://discord.gg/nDceKgxnkV). diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-NO.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-NO.md new file mode 100644 index 0000000..89a5745 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-NO.md @@ -0,0 +1,46 @@ +# Bidrag til RustDesk + +RustDesk er Ã¥pene for bidrag fra alle. Her er reglene for de som har lyst til Ã¥ +hjelpe oss: + +## Bidrag + +Bidrag til RustDesk eller deres avhengigheter burde være i form av GitHub pull requests. +Hver pull request vill bli sett igjennom av en kjerne bidrager (noen med autoritet til +Ã¥ godkjenne endringene) og enten bli sendt til main treet eller respondert med +tilbakemelding pÃ¥ endringer som er nødvendig. Alle bidrag burde følge dette formate +ogsÃ¥ de fra kjerne bidragere. + +Om du ønsker Ã¥ jobbe pÃ¥ en issue mÃ¥ du huske Ã¥ gjøre krav pÃ¥ den først. Dette +kann gjøres ved Ã¥ kommentere pÃ¥ den GitHub issue-en du ønsker Ã¥ jobbe pÃ¥. +Dette er for Ã¥ hindre duplikat innsats pÃ¥ samme problem. + +## Pull Request Sjekkliste + +- Lag en gren fra master grenen og, hvis det er nødvendig, rebase den til den nÃ¥værende + master grenen før du sender inn din pull request. Hvis ikke dette gjøres pÃ¥ rent + vis vill du bli spurt om Ã¥ rebase dine endringer. + +- Commits burde være sÃ¥ smÃ¥ som mulig, samtidig som de mÃ¥ være korrekt uavhenging av hverandre + (hver commit burde kompilere og bestÃ¥ tester). + +- Commits burde være akkopaniert med en Developer Certificate of Origin + (http://developercertificate.org), som indikerer att du (og din arbeidsgiver + i det tilfellet) godkjenner Ã¥ bli knyttet til vilkÃ¥rene av [prosjekt lisensen](../LICENCE). + Ved bruk av git er dette `-s` opsjonen til `git commit`. + +- Hvis dine endringer ikke blir sett eller hvis du trenger en spesefik person til + Ã¥ se pÃ¥ dem kan du @-svare en med autoritet til Ã¥ godkjenne dine endringer. + Dette kann gjøres i en pull request, en kommentar eller via epost pÃ¥ [email](mailto:info@rustdesk.com). + +- Legg til tester relevant til en fikset bug eller en ny tilgjengelighet. + +For spesefike git instruksjoner, se [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Oppførsel + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Kommunikasjon + +RustDesk bidragere burker [Discord](https://discord.gg/nDceKgxnkV). diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-PL.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-PL.md new file mode 100644 index 0000000..8341692 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-PL.md @@ -0,0 +1,45 @@ +# Współtworzenie RustDesk + +RustDesk z zadowoleniem przyjmuje wkÅ‚ad od każdego. Oto wytyczne, jeÅ›li chcesz nam pomóc: + +## Współtwórcy + +Contributions to RustDesk or its dependencies should be made in the form of GitHub +pull requests. Each pull request will be reviewed by a core contributor +(someone with permission to land patches) and either landed in the main tree or +given feedback for changes that would be required. All contributions should +follow this format, even those from core contributors. + +Should you wish to work on an issue, please claim it first by commenting on +the GitHub issue that you want to work on it. This is to prevent duplicated +efforts from contributors on the same issue. + +## Pull Request Checklist + +- Branch from the master branch and, if needed, rebase to the current master + branch before submitting your pull request. If it doesn't merge cleanly with + master you may be asked to rebase your changes. + +- Commits should be as small as possible, while ensuring that each commit is + correct independently (i.e., each commit should compile and pass tests). + +- Commits should be accompanied by a Developer Certificate of Origin + (http://developercertificate.org) sign-off, which indicates that you (and + your employer if applicable) agree to be bound by the terms of the + [project license](../LICENCE). In git, this is the `-s` option to `git commit` + +- If your patch is not getting reviewed or you need a specific person to review + it, you can @-reply a reviewer asking for a review in the pull request or a + comment, or you can ask for a review via [email](mailto:info@rustdesk.com). + +- Add tests relevant to the fixed bug or new feature. + +For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Kodeks postÄ™powania + +[Kodeks postÄ™powania](CODE_OF_CONDUCT-PL.md) + +## Komunikacja + +RustDesk contributors frequent the [Discord](https://discord.gg/nDceKgxnkV). diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-RO.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-RO.md new file mode 100644 index 0000000..8249fb8 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-RO.md @@ -0,0 +1,31 @@ +# ContribuÈ›ii la RustDesk + +RustDesk primeÈ™te cu plăcere contribuÈ›ii din partea tuturor. Iată ghidurile dacă te gândeÈ™ti să ne ajuÈ›i: + +## ContribuÈ›ii + +ContribuÈ›iile la RustDesk sau la dependenÈ›ele sale ar trebui făcute sub forma de pull request-uri pe GitHub. Fiecare pull request va fi revizuit de un contributor principal (cineva cu permisiunea de a aplica patch-uri) È™i fie va fi integrat în arborele principal, fie vor fi oferite sugestii pentru modificările necesare. Toate contribuÈ›iile trebuie să urmeze acest format, chiar È™i cele ale contributorilor principali. + +Dacă doreÈ™ti să lucrezi la o problemă, te rugăm să o revendici mai întâi comentând pe GitHub issue-ul pe care vrei să lucrezi. Aceasta previne eforturi duplicate din partea contributorilor asupra aceleiaÈ™i probleme. + +## Lista de verificare pentru Pull Request + +- Creează un branch din branch-ul `master` È™i, dacă este necesar, fă rebase la branch-ul `master` curent înainte de a trimite pull request-ul. Dacă nu se poate integra curat cu `master`, È›i se poate cere să faci rebase la modificările tale. + +- Commit-urile ar trebui să fie cât mai mici posibil, asigurând totodată că fiecare commit este corect independent (adică fiecare commit ar trebui să compileze È™i să treacă testele). + +- Commit-urile trebuie să fie însoÈ›ite de un semnătura Developer Certificate of Origin (http://developercertificate.org), care indică faptul că tu (È™i angajatorul tău, dacă este cazul) eÈ™ti de acord să respecÈ›i termenii [licenÈ›ei proiectului](../LICENCE). ÃŽn git, aceasta este opÈ›iunea `-s` la `git commit`. + +- Dacă patch-ul tău nu este revizuit sau ai nevoie ca o anumită persoană să-l revizuiască, poÈ›i @-reply unui reviewer cerând o revizuire în pull request sau într-un comentariu, sau poÈ›i solicita o revizuire prin [email](mailto:info@rustdesk.com). + +- Adaugă teste relevante pentru bug-ul corectat sau pentru funcÈ›ionalitatea nouă. + +Pentru instrucÈ›iuni specifice git, vezi [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Conduită + +[Codul de Conduită RustDesk](https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md) + +## Comunicare + +Contributorii RustDesk frecventează [Discord](https://discord.gg/nDceKgxnkV). diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-RU.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-RU.md new file mode 100644 index 0000000..1cf9a47 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-RU.md @@ -0,0 +1,45 @@ +# Вклад в RustDesk + +RustDesk приветÑтвует вклад каждого. +Ðиже приведены рекомендации, еÑли вы ÑобираетеÑÑŒ помочь нам: + +## Вклад в развитие + +Вклады в развитие RustDesk или его завиÑимоÑти должны быть Ñделаны в виде `pull request` на GitHub. +Каждый такой `pull request` будет раÑÑмотрен оÑновным учаÑтником (кем-то, у кого еÑть разрешение +на влив иÑправлений) и либо помещен в оÑновное дерево, либо Вам будет дан отзыв о необходимых правках. +Ð’Ñе материалы должны ÑоответÑтвовать Ñтому формату, даже те, которые поÑтупают от оÑновных авторов. + +ЕÑли вы хотите поработать над какой-либо проблемой, то пожалуйÑта, Ñначала напишите об Ñтом, +Ñоздав `issue` на GitHub, и опиÑав, над чем вы хотите поработать. Это делаетÑÑ Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, +чтобы предотвратить дублирование уÑилий учаÑтников по одному и тому же вопроÑу. + +## Контрольный ÑпиÑок Ð´Ð»Ñ Ð’Ð°ÑˆÐ¸Ñ… `pull request` + +- ОтветвлÑйтеÑÑŒ от главной ветки и, при необходимоÑти, делайте `rebase` в текущую `master` + ветку перед отправкой `pull request`. При наличии конфликтов ÑлиÑÐ½Ð¸Ñ Ð²Ð°Ð¼ будет + предложено их уÑтранить, возможно при помощи того же `rebase`. + +- Коммиты должны быть, по возможноÑти, небольшими, при Ñтом гарантируÑ, что каждый + коммит ÑвлÑетÑÑ Ð½ÐµÐ·Ð°Ð²Ð¸Ñимо правильным (Ñ‚.е., каждый коммит должен компилироватьÑÑ Ð¸ проходить теÑты). + +- Коммиты должны ÑопровождатьÑÑ Ð¿Ð¾Ð´Ð¿Ð¸Ñью `Developer Certificate of Origin` + (http://developercertificate.org), ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ ÑƒÐºÐ°Ð¶ÐµÑ‚ на то, что вы (и ваш работодатель, + еÑли Ñто применимо) ÑоглаÑны Ñоблюдать уÑÐ»Ð¾Ð²Ð¸Ñ [лицензии проекта](../LICENCE). + Ð’ `git` Ñто флаг `-s` при иÑпользовании `git commit` + +- ЕÑли ваш патч не проходит рецензирование или вам нужно, + чтобы его проверил конкретный человек, Ð’Ñ‹ можете ответить рецензенту через `@`, + в обÑуждениÑÑ… вашего `pull request` или Ð’Ñ‹ можете запроÑить рецензию через[email](mailto:info@rustdesk.com). + +- Добавьте теÑты, отноÑÑщиеÑÑ Ðº иÑправленной ошибке или новой функции. + +Ð”Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ ÐºÐ¾Ð½ÐºÑ€ÐµÑ‚Ð½Ñ‹Ñ… инÑтрукций `git` Ñм. [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow). + +## Правила Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ ÑƒÑ‡Ð°Ñтников и вкладчиков + +Ðормы Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð²Ð½ÑƒÑ‚Ñ€Ð¸ ÑообщеÑтва подробно опиÑаны [здеÑÑŒ](CODE_OF_CONDUCT-RU.md). + +## Общение + +RustDesk контрибьюторы могут поÑетить [Discord](https://discord.gg/nDceKgxnkV). diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-TR.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-TR.md new file mode 100644 index 0000000..6e9e3f3 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-TR.md @@ -0,0 +1,31 @@ +# RustDesk'a Katkı SaÄŸlamak + +RustDesk, herkesten katkıyı memnuniyetle karşılar. EÄŸer bize yardımcı olmayı düşünüyorsanız, iÅŸte rehberlik eden kurallar: + +## Katkılar + +RustDesk veya bağımlılıklarına yapılan katkılar, GitHub pull istekleri ÅŸeklinde yapılmalıdır. Her bir pull isteÄŸi, çekirdek katkıcı tarafından gözden geçirilecek (yamaları kabul etme izni olan biri) ve ana aÄŸaca kabul edilecek veya gerekli deÄŸiÅŸiklikler için geri bildirim verilecektir. Tüm katkılar bu formata uymalıdır, çekirdek katkıcılardan gelenler bile. + +EÄŸer bir konu üzerinde çalışmak isterseniz, önce üzerinde çalışmak istediÄŸinizi belirten bir yorum yaparak konuyu talep ediniz. Bu, katkı saÄŸlayanların aynı konuda çift çalışmasını engellemek içindir. + +## Pull İstek Kontrol Listesi + +- Master dalından dallandırın ve gerekiyorsa pull isteÄŸinizi göndermeden önce mevcut master dalına rebase yapın. EÄŸer master ile temiz bir ÅŸekilde birleÅŸmezse, deÄŸiÅŸikliklerinizi rebase yapmanız istenebilir. + +- Her bir commit mümkün olduÄŸunca küçük olmalıdır, ancak her commit'in bağımsız olarak doÄŸru olduÄŸundan emin olun (örneÄŸin, her commit derlenebilir ve testleri geçmelidir). + +- Commit'ler, bir GeliÅŸtirici Sertifikası ile desteklenmelidir (http://developercertificate.org). Bu, [proje lisansının](../LICENCE) koÅŸullarına uymayı kabul ettiÄŸinizi gösteren bir onaydır. Git'te bunu `git commit` seçeneÄŸi olarak `-s` seçeneÄŸi ile yapabilirsiniz. + +- Yamalarınız gözden geçirilmiyorsa veya belirli bir kiÅŸinin gözden geçirmesine ihtiyacınız varsa, çekme isteÄŸi veya yorum içinde bir gözden geçirmeyi istemek için bir inceleyiciyi @etiketleyebilir veya inceleme için [e-posta](mailto:info@rustdesk.com) ile talep edebilirsiniz. + +- DüzelttiÄŸiniz hatanın veya eklediÄŸiniz yeni özelliÄŸin ilgili testlerini ekleyin. + +Daha spesifik git talimatları için, [GitHub iÅŸ akışı 101](https://github.com/servo/servo/wiki/GitHub-workflow)'e bakınız. + +## Davranış + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-TR.md + +## İletiÅŸim + +RustDesk katkı saÄŸlayıcıları, [Discord](https://discord.gg/nDceKgxnkV) kanalını sık sık ziyaret ederler. diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING-ZH.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-ZH.md new file mode 100644 index 0000000..718cdac --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING-ZH.md @@ -0,0 +1,32 @@ +# 为RustDeskåšè´¡çŒ® + +Rust欢迎æ¯ä¸€ä½è´¡çŒ®è€…,如果您有æ„å‘为我们åšå‡ºè´¡çŒ®ï¼Œè¯·éµå¾ªä»¥ä¸‹æŒ‡å—: + +## è´¡çŒ®æ–¹å¼ + +对 RustDesk 或其ä¾èµ–项的贡献需è¦é€šè¿‡ GitHub çš„ Pull Request (PR) çš„å½¢å¼æäº¤ã€‚æ¯ä¸ª PR éƒ½ä¼šç”±æ ¸å¿ƒè´¡çŒ®è€…ï¼ˆå³æœ‰æƒé™åˆå¹¶ä»£ç çš„人)进行审核,审核通过åŽä»£ç ä¼šåˆå¹¶åˆ°ä¸»åˆ†æ”¯ï¼Œæˆ–者您会收到需è¦ä¿®æ”¹çš„å馈。所有贡献者,包括核心贡献者,æäº¤çš„代ç éƒ½åº”éµå¾ªæ­¤æµç¨‹ã€‚ + +å¦‚æžœæ‚¨å¸Œæœ›å¤„ç†æŸä¸ªé—®é¢˜ï¼Œè¯·å…ˆåœ¨å¯¹åº”çš„ GitHub issue 下å‘表评论,声明您将处ç†è¯¥é—®é¢˜ï¼Œä»¥é¿å…该问题被多ä½è´¡çŒ®è€…é‡å¤å¤„ç†ã€‚ + +## PR 注æ„事项 + +- 从 master 分支创建一个新的分支,并在æäº¤PR之å‰ï¼Œå¦‚果需è¦ï¼Œå°†æ‚¨çš„分支 å˜åŸº(rebase) 到最新的 master 分支。如果您的分支无法顺利åˆå¹¶åˆ° master 分支,您å¯èƒ½ä¼šè¢«è¦æ±‚更新您的代ç ã€‚ + +- æ¯æ¬¡æäº¤çš„æ”¹åŠ¨åº”è¯¥å°½å¯èƒ½å°‘,并且è¦ä¿è¯æ¯æ¬¡æäº¤çš„代ç éƒ½æ˜¯æ­£ç¡®çš„ï¼ˆå³æ¯ä¸ª commit 都应能æˆåŠŸç¼–è¯‘å¹¶é€šè¿‡æµ‹è¯•ï¼‰ã€‚ + +- æ¯ä¸ªæäº¤éƒ½åº”附有开å‘者è¯ä¹¦ç­¾å(http://developercertificate.org), è¡¨æ˜Žæ‚¨ï¼ˆä»¥åŠæ‚¨çš„é›‡ä¸»ï¼Œè‹¥é€‚ç”¨ï¼‰åŒæ„éµå®ˆé¡¹ç›®[许å¯è¯æ¡æ¬¾](../LICENCE)。在使用 git æäº¤ä»£ç æ—¶ï¼Œå¯ä»¥é€šè¿‡åœ¨ `git commit` 时使用 `-s` 选项加入签å + +- 如果您的 PR æœªè¢«åŠæ—¶å®¡æ ¸ï¼Œæˆ–éœ€è¦æŒ‡å®šçš„人员进行审核,您å¯ä»¥é€šè¿‡åœ¨ PR 或评论中 @ æåˆ°ç›¸å…³å®¡æ ¸è€…,以åŠå‘é€[电å­é‚®ä»¶](mailto:info@rustdesk.com)的方å¼è¯·æ±‚审核。 + +- 请为修å¤çš„ bug 或新增的功能添加相应的测试用例。 + +有关具体的 git 使用说明,请å‚考[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## 行为准则 + +请éµå®ˆé¡¹ç›®çš„[贡献者公约行为准则](./CODE_OF_CONDUCT-ZH.md)。 + + +## æ²Ÿé€šæ¸ é“ + +RustDesk 的贡献者主è¦é€šè¿‡ [Discord](https://discord.gg/nDceKgxnkV) 进行交æµã€‚ diff --git a/shelled/rustdesk-as-ref/docs/CONTRIBUTING.md b/shelled/rustdesk-as-ref/docs/CONTRIBUTING.md new file mode 100644 index 0000000..31fd632 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing to RustDesk + +RustDesk welcomes contribution from everyone. Here are the guidelines if you are +thinking of helping us: + +## Contributions + +Contributions to RustDesk or its dependencies should be made in the form of GitHub +pull requests. Each pull request will be reviewed by a core contributor +(someone with permission to land patches) and either landed in the main tree or +given feedback for changes that would be required. All contributions should +follow this format, even those from core contributors. + +Should you wish to work on an issue, please claim it first by commenting on +the GitHub issue that you want to work on it. This is to prevent duplicated +efforts from contributors on the same issue. + +## Pull Request Checklist + +- Branch from the master branch and, if needed, rebase to the current master + branch before submitting your pull request. If it doesn't merge cleanly with + master you may be asked to rebase your changes. + +- Commits should be as small as possible, while ensuring that each commit is + correct independently (i.e., each commit should compile and pass tests). + +- Commits should be accompanied by a Developer Certificate of Origin + (http://developercertificate.org) sign-off, which indicates that you (and + your employer if applicable) agree to be bound by the terms of the + [project license](../LICENCE). In git, this is the `-s` option to `git commit` + +- If your patch is not getting reviewed or you need a specific person to review + it, you can @-reply a reviewer asking for a review in the pull request or a + comment, or you can ask for a review via [email](mailto:info@rustdesk.com). + +- Add tests relevant to the fixed bug or new feature. + +For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Conduct + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Communication + +RustDesk contributors frequent the [Discord](https://discord.gg/nDceKgxnkV). diff --git a/shelled/rustdesk-as-ref/docs/README-AR.md b/shelled/rustdesk-as-ref/docs/README-AR.md new file mode 100644 index 0000000..5aa09da --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-AR.md @@ -0,0 +1,173 @@ +

+ RustDesk - Your remote desktop
+ Servers • + Build • + Docker • + Structure • + Snapshot
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [Tiếng Việt] | [Ελληνικά]
+ لغتك الأم, Doc و RustDesk UI, README نحن بحاجة إلى مساعدتك لترجمة هذا +

+ +[Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) :تواصل معنا عبر + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%D8%A7%D9%84%D9%85%D9%8A%D8%B2%D8%A7%D8%AA%20%D8%A7%D9%84%D9%85%D8%AA%D9%82%D8%AF%D9%85%D8%A9-blue)](https://rustdesk.com/pricing.html) + +.Rustبرنامج آخر لسطح المكتب عن بعد، مكتوب بـ +يعمل خارج الصندوق، لا حاجة إلى إعدادات. لديك سيطرة كاملة على بياناتك، دون مخاو٠بشأن الأمن. يمكنك استخدام خادم + الخاص بنا rendezvous/relay +[جهز Ù„Ù†ÙØ³Ùƒ واحدا](https://rustdesk.com/server), أو +[خاص بك rendezvous/relay أكتب خادم](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +لمساعدتك على ذلك [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) يرحب بمساهمة الجميع. اطلع على RustDesk. + +[**ØŸ RustDesk كيÙية يعمل**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) + +[**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases) + + +## التبعيات + + لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم + Ø¨Ù†ÙØ³Ùƒ sciter dynamic library عليك تحميل + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + + Sciter إلى Flutter سنقوم بترحيل نسخة سطح المكتب من .Flutter تستخدم إصدارات الهات٠المحمول. + +## خطوات البناء + +- C++ build env Ùˆ Rust development env قم بإعداد + +- بطريقة صحيحة `VCPKG_ROOT` env variable وأعد [vcpkg](https://github.com/microsoft/vcpkg) ثبت + + - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static` + - Linux/MacOS: `vcpkg install libvpx libyuv opus aom` + +- run `cargo run` + +## [البناء](https://rustdesk.com/docs/en/dev/build/) + +## Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + + +### vcpkg تثبيت + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Fix libvpx (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### البناء + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Docker طريقة البناء باستخدام + +ابدأ باستنساخ المستودع وبناء الكونتاينر: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +ثم، ÙÙŠ كل مرة تحتاج إلى بناء التطبيق، قم بتشغيل الأمر التالي: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +لاحظ أن البناء الأول قد يستغرق وقتًا أطول قبل تخزين التبعيات، وسيكون البناء اللاحق أسرع. Ø¨Ø§Ù„Ø¥Ø¶Ø§ÙØ© إلى ذلك، إذا كنت بحاجة إلى تحديد وسائط Ù…Ø®ØªÙ„ÙØ© لأمر البناء، Ùيمكنك القيام بذلك ÙÙŠ نهاية الأمر بوضع +`` +على سبيل المثال، إذا كنت ترغب ÙÙŠ بناء إصدار محسن، ÙØ³ØªÙ‚وم بتشغيل الأمر أعلاه متبوعًا بـ +`--release` +:سيكون المل٠القابل للتنÙيذ الناتج متاحًا ÙÙŠ مجلد تارغت، ويمكن تشغيله باستخدام + +```sh +target/debug/rustdesk +``` + +:أو ÙÙŠ حال قمت ببناء إصدار محسن + +```sh +target/release/rustdesk +``` + +RustDesk يرجى التأكد من أنك ØªÙ†ÙØ° هذه الأوامر من جذر مستودع +وإلا Ùقد لا يتمكن التطبيق من العثور على الموارد المطلوبة. لاحظ أيضًا أن الأوامر Ø§Ù„ÙØ±Ø¹ÙŠØ© الأخرى مثل +`install` أو `run` +لا يتم دعمها حاليًا عبر هذه الطريقة لأنها ستقوم بتثبيت أو تشغيل البرنامج داخل الكونتاينر بدلاً من الهوست. + +## هيكل المل٠+ +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: وظائ٠لنقل Ø§Ù„Ù…Ù„ÙØ§ØªØŒ وبعض وظائ٠المراÙÙ‚ الأخرى tcp/udpØŒ protobuf ترميز الÙيديو، إعدادات + +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: التقاط الشاشة +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: التحكم ÙÙŠ لوحة Ø§Ù„Ù…ÙØ§ØªÙŠØ­/الماوس الخاصة بكل منصة +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: واجهة المستخدم الرسومية +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: خدمات الصوت/Ø§Ù„Ø­Ø§ÙØ¸Ø©/المدخلات/الÙيديو، ووصلات الشبكة +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: بدء اتصال متقارن +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: أو المنقول عن Ø¨ÙØ¹Ø¯ (TCP hole punching) انتظر الاتصال المباشر [rustdesk-server](https://github.com/rustdesk/rustdesk-server) الإتصال ب +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: رمز خاص بكل منصة +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: رمز الهات٠المحمول +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**:Flutter لعميل الويب الخاص ب Javascript + +## لقطات + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-CS.md b/shelled/rustdesk-as-ref/docs/README-CS.md new file mode 100644 index 0000000..b208414 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-CS.md @@ -0,0 +1,157 @@ +

+ RustDesk – vaše vzdálená plocha
+ Servery • + Sestavení ze zdrojových kódů • + Docker • + Struktura • + Ukázky
+ [English] | [УкраїнÑька] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Potřebujeme Vaši pomoc s překladem tohoto README, uživatelského rozhraní aplikace RustDesk a dokumentace k ní do vašeho jazyka +

+ +Popovídejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Pokro%C4%8Dil%C3%A9%20Funkce-blue)](https://rustdesk.com/pricing.html) + +Zase další software pro přístup k ploÅ¡e na dálku, naprogramovaný v jazyce Rust. Funguje hned tak, jak je – není tÅ™eba žádného nastavování. Svá data máte ve svých rukách, bez obav o zabezpeÄení. Je možné používat námi poskytovaný propojovací/pÅ™edávací (relay) server, [vytvoÅ™it si svůj vlastní](https://rustdesk.com/server), nebo [si dokonce svůj vlastní naprogramovat](https://github.com/rustdesk/rustdesk-server-demo), budete-li chtít. + +Projekt RustDesk vítá pÅ™iložení ruky k dílu od každého. Jak zaÄít se dozvíte z [`docs/CONTRIBUTING.md`](CONTRIBUTING.md). + +[**Jak RustDesk funguje?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) + +[**STAHOVÃNà ZKOMPILOVANÃCH APLIKACÃ**](https://github.com/rustdesk/rustdesk/releases) + +## Softwarové souÄásti, na kterých závisí + +Varianta pro poÄítaÄ používá pro grafické uživatelské rozhraní [sciter](https://sciter.com/) – stáhnÄ›te si potÅ™ebnou knihovnu. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +Varianta pro mobilní platformy používá aplikaÄní rámec (framework) Flutter. Na tu také v budoucnu pÅ™edÄ›láme i variantu pro poÄítaÄ. + +## StruÄnÄ› kroky pro sestavení ze zdrojových kódů + +- PÅ™ipravte si vývojové prostÅ™edí pro jazyky Rust a C++ + +- Nainstalujte [vcpkg](https://github.com/microsoft/vcpkg), a správnÄ› nastavte promÄ›nnou prostÅ™edí `VCPKG_ROOT` + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- spusÅ¥te `cargo run` + +## [Sestavení ze zdrojových kódů](https://rustdesk.com/docs/en/dev/build/) + +## Jak zkompilovat na Linuxu + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Instalace vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Oprava libvpx (pro Fedoru) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Sestavení + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Jak sestavit prostÅ™ednictvím Docker kontejnerizace + +ZaÄnÄ›te tím, že si naklonujete tento repozitář a sestavíte docker kontejner: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Poté pokaždé, když bude tÅ™eba aplikaci sestavit, spusÅ¥te následující příkaz: + +```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 +``` + +VÅ¡imnÄ›te si, že prvotní sestavení může trvat déle (než se do mezipamÄ›ti uloží veÅ¡keré softwarové souÄásti, které jsou potÅ™eba) – následná opakování už budou rychlejší. Pokud navíc potÅ™ebujete zadat různé argumenty příkazu pro sestavení, můžete tak uÄinit na konci příkazu v pozici ``. Například, pokud byste chtÄ›li sestavit optimalizovanou verzi pro vydání, spustili byste výše uvedený příkaz následovaný `--release`. Výsledný spustitelný soubor se objeví v cílové složce na vaÅ¡em systému a bude ho možné spustit pomocí: + +```sh +target/debug/rustdesk +``` + +Nebo, pokud spouÅ¡títe variantu pro vydání: + +```sh +target/release/rustdesk +``` + +UjistÄ›te se, že tyto příkazy spouÅ¡títe z koÅ™enového adresáře RustDesk, jinak aplikace nemusí být schopná nalézt potÅ™ebné prostÅ™edky (resources). Také si vÅ¡imnÄ›te, že ostatní dílÄí príkazy nástroje cargo, jako tÅ™eba `install` nebo `run` zatím nejsou prostÅ™ednictvím této metody podporovány, protože by vedly k instalaci Äi spuÅ¡tÄ›ní program uvnitÅ™ kontejneru namísto přímo v systému. + +## Struktura souborů + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: kodek videa, nastavení, obalovaní tcp/udp, vyrovnávací paměť protokolu, funkce souborového systému pro pÅ™enos souborů a pár dalších podpůrných funkcí +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: zachytávání obsahu obrazovky +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: ovládání klávesnice/myÅ¡i pro jednotlivé platformy +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: grafické uživatelské rozhraní +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: služby pro zvuk/schránku/zadávání/video a síťová spojení +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: spouÅ¡tí pÅ™ipojení k protÄ›jÅ¡ku +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: komunikace s [rustdesk-server](https://github.com/rustdesk/rustdesk-server), oÄekávání vzdálených příméhých („prodÄ›rováváním“ TCP) nebo pÅ™edávaných (relay) spojení +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: zdrojové kódy, specifické pro jednotlivé platformy +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: zdrojové kódy pro použití s aplikaÄním rámcem (framework) Flutter pro mobilní platformy +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript pro Flutter webový klient + +## Ukázky + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-DA.md b/shelled/rustdesk-as-ref/docs/README-DA.md new file mode 100644 index 0000000..9ad109d --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-DA.md @@ -0,0 +1,149 @@ +

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

+ +Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Avancerede%20Funktioner-blue)](https://rustdesk.com/pricing.html) + +Endnu en fjernskrivebordssoftware, skrevet i Rust. Fungerer ud af æsken, ingen konfiguration påkrævet. Du har fuld kontrol over dine data uden bekymringer om sikkerhed. Du kan bruge vores rendezvous/relay-server, [opsætte din egen](https://rustdesk.com/server), eller [skrive din egen rendezvous/relay-server](https://github.com/rustdesk/rustdesk- server-demo). + +RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) for at få hjælp til at komme i gang. + +[**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +## Afhængigheder + +Desktopversioner bruger [sciter](https://sciter.com/) eller Flutter til GUI, denne vejledning er kun for Sciter. + +Hent venligst sciter dynamic library selv. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Rå trin til at bygge + +- Forbered din Rust-udviklings-env og C++ build-env + +- Installer [vcpkg](https://github.com/microsoft/vcpkg), og indstil env-variabelen "VCPKG_ROOT" korrekt + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- kør `cargo run` + +## [Byg](https://rustdesk.com/docs/en/dev/build/) + +## Sådan bygger du på Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### vcpkg installation + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### libvpx rettelse (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Byg + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +cargo run +``` + +## Sådan bygger du med Docker + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Kør derefter følgende kommando, hver gang du skal bygge applikationen: +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Bemærk, at den første bygning kan tage længere tid, før afhængigheder cachelagres, efterfølgende bygninger vil være hurtigere. Derudover, hvis du har brug for at angive forskellige argumenter til bygge-kommandoen, kan du gøre det i slutningen af kommandoen i ``-positionen. For eksempel, hvis du ville bygge en optimeret udgivelsesversion, ville du køre kommandoen ovenfor efterfulgt af `--release`. Den resulterende eksekverbare vil være tilgængelig i målmappen på dit system og kan køres med: + +```sh +target/debug/rustdesk +``` + +Eller, hvis du kører en udgivelses eksekverbar: + +```sh +target/release/rustdesk +``` + +Sørg for, at du kører disse kommandoer fra roden af RustDesk-lageret, ellers kan applikationen muligvis ikke finde de nødvendige ressourcer. Bemærk også, at andre cargo underkommandoer såsom 'install' eller 'run' i øjeblikket ikke understøttes via denne metode, da de ville installere eller køre programmet inde i containeren i stedet for værten. + +## Filstruktur + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs funktioner til filoverførsel og nogle andre hjælpefunktioner +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Skærmbillede +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specifik tastatur/mus kontrol +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: lyd/udklipsholder/input/videotjenester og netværksforbindelser +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: starte en peer-forbindelse +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommuniker med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernforbindelse (TCP-hulning) eller relæforbindelse +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Javascript til Flutter webklient + +## Skærmbilleder + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-DE.md b/shelled/rustdesk-as-ref/docs/README-DE.md new file mode 100644 index 0000000..ba88944 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-DE.md @@ -0,0 +1,182 @@ +

+ RustDesk - Dein Remote-Desktop
+ Kompilieren • + Docker • + Dateistruktur • + Screenshots
+ [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk] | [Română]
+ Wir brauchen Ihre Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in Ihre Muttersprache zu übersetzen. +

+ +> [!Caution] +> **Haftungsausschluss bei Missbrauch::**
+> Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung. + + +Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Erweiterte%20Funktionen-blue)](https://rustdesk.com/pricing.html) + +RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Sie haben die volle Kontrolle über Ihre Daten und müssen sich keine Sorgen um die Sicherheit machen. Sie können unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk heißt jegliche Mitarbeit willkommen. Schauen Sie sich [CONTRIBUTING-DE.md](CONTRIBUTING-DE.md) an, wenn Sie Unterstützung beim Start brauchen. + +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases) + +[**Nightly Builds**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) + +## Abhängigkeiten + +Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter. + +Bitte laden Sie die dynamische Bibliothek Sciter selbst herunter. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Grobe Schritte zum Kompilieren + +- Bereiten Sie Ihre Rust-Entwicklungsumgebung und C++-Build-Umgebung vor + +- Installieren Sie [vcpkg](https://github.com/microsoft/vcpkg) und fügen Sie die Systemumgebungsvariable `VCPKG_ROOT` hinzu + + - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static` + - Linux/macOS: `vcpkg install libvpx libyuv opus aom` + +- Nutzen Sie `cargo run` + +## [Erstellen](https://rustdesk.com/docs/de/dev/build/) + +## Kompilieren auf Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### vcpkg installieren + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### libvpx reparieren (für Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Kompilieren + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone --recurse-submodules https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Auf Docker kompilieren + +Beginnen Sie damit, das Repository zu klonen und den Docker-Container zu bauen: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +git submodule update --init --recursive +docker build -t "rustdesk-builder" . +``` + +Führen Sie jedes Mal, wenn Sie das Programm kompilieren müssen, folgenden Befehl aus: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Bedenken Sie, dass das erste Kompilieren länger dauern kann, bis die Abhängigkeiten zwischengespeichert sind. Nachfolgende Kompiliervorgänge sind schneller. Wenn Sie verschiedene Argumente für den Kompilierbefehl angeben müssen, können Sie dies am Ende des Befehls an der Position `` tun. Wenn Sie zum Beispiel eine optimierte Releaseversion kompilieren wollen, können Sie `--release` am Ende des Befehls anhängen. Das daraus entstehende Programm finden Sie im Zielordner auf Ihrem System. Sie können es mit folgendem Befehl ausführen: + +```sh +target/debug/rustdesk +``` + +Oder, wenn Sie eine Releaseversion benutzen: + +```sh +target/release/rustdesk +``` + +Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzen. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenken Sie auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf Ihrem eigentlichen System. + +## Dateistruktur + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatursteuerung +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Datei kopieren und einfügen Implementierung für Windows, Linux, macOS. +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerkverbindungen +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript für Flutter-Webclient + +## Screenshots + +![Verbindungsmanager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![Verbunden zu einem Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![Dateiübertragung](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP-Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) + diff --git a/shelled/rustdesk-as-ref/docs/README-EO.md b/shelled/rustdesk-as-ref/docs/README-EO.md new file mode 100644 index 0000000..d2a9315 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-EO.md @@ -0,0 +1,148 @@ +

+ RustDesk - Your remote desktop
+ Serviloj • + Kompili • + Docker • + Strukturo • + Ekrankopio
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Ni bezonas helpon traduki tiun README kaj la interfacon al via denaska lingvo +

+ +Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Altnivela%20Funkcioj-blue)](https://rustdesk.com/pricing.html) + +Denove alia fora labortabla programo, skribita en Rust. Äœi funkcias elskatole, ne bezonas konfiguraĵon. Vi havas la tutan kontrolon sur viaj datumoj, sen zorgo pri sekureco. Vi povas uzi nian servilon rendezvous/relajsan, [agordi vian propran](https://rustdesk.com/server), aÅ­ [skribi vian propran servilon rendezvous/relajsan](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) por helpo komenci. + +[**BINARA ELÅœUTO**](https://github.com/rustdesk/rustdesk/releases) + +## Dependantaĵoj + +La labortabla versio uzas [sciter](https://sciter.com/) por la interfaco, bonvolu elÅuti la bibliotekon dinamikan sciter. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## PaÅoj por kompili + +- Preparu vian medion de programado Rust kaj vian medion de kompilado C++ + +- Instalu [vcpkg](https://github.com/microsoft/vcpkg), kaj agordu la medivariablon `VCPKG_ROOT` korekte + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- Plenumu `cargo run` + +## Kiel kompili sur Linukso + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Instali vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Ripari libvpx (Por Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Kompili + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Kiel kompili kun Docker + +Komencu klonante la deponejon kaj kompilu la konteneron Docker: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Tiam, ĉiuj fojoj, kiuj vi bezonas kompili la programon, plenumu tiun komandon: + +```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 +``` + +Notu, ke la unua kompilado povas daÅ­ri longe, antaÅ­ ke la dependantaĵoj estu kaÅitaj, sekvaj kompiladoj estos pli rapidaj. Aldone, se vi bezonas specifi diferentajn argumentojn por la kompilkomando, vi povas fari Äin en la fine de la komando, en la posicio ``. Ekzemple, se vi volas kompili version de eldono optimigita, vi plenumus la komandon supre, kun `--release`. La plenumebla dosiero disponeblos en la cela dosierujo sur via sistemo, kaj povos esti plenumita kun: + +```sh +target/debug/rustdesk +``` + +AÅ­, se vi plenumas eldonan plenumeblan dosieron: + +```sh +target/release/rustdesk +``` + +Bonvolu certigi, ke vi plenumas tiujn komandojn el la radiko de la deponejo RustDesk, alie la programo povus esti nekapabla de trovi la devigajn resursojn. AnkaÅ­ notu, ke la aliaj subkomandoj de cargo kiel `install` aÅ­ `run` momente ne estas subtenitaj per tiu metodo, ĉar instalus aÅ­ plenumus la programon en la kontenero anstataÅ­ de la gastiganto. + +## Dosierstrukturo + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: videa kodeko, agordado, kovrilo tcp/udp, protobuf, funkcioj fs por dosiertransigo, kaj aliaj utilaĵaj funkcioj +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ekrankaptado +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: operaciumspecifa kontrolo de klavaro/muso +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: interfaco +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: aÅ­dio/poÅo/enigo/videa servoj, kaj retkonektoj +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: starti konekto kun samtavolo +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: komuniki kun [rustdesk-server](https://github.com/rustdesk/rustdesk-server), atendi foran direktan (TCP hole punching) aÅ­ relajsatan konekton +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: operaciumspecifa kodo + +## Ekrankopio + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-ES.md b/shelled/rustdesk-as-ref/docs/README-ES.md new file mode 100644 index 0000000..da939bd --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-ES.md @@ -0,0 +1,180 @@ +

+ RustDesk - Your remote desktop
+ Servidores • + Compilar • + Docker • + Estructura • + Capturas de pantalla
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Necesitamos tu ayuda para traducir este README a tu idioma +

+ +> [!Caution] +> **Descargo de responsabilidad por mal uso:**
+> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El mal uso, como el acceso no autorizado, el control o la invasión de la privacidad, va estrictamente en contra de nuestras directrices. Los autores no se hacen responsables de ningún uso indebido de la aplicación. + +Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Caracter%C3%ADsticas%20Avanzadas-blue)](https://rustdesk.com/pricing.html) + +Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de tus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [instalar el tuyo](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda para empezar. + +[**¿Cómo funciona rustdesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) + +[**DESCARGA DE BINARIOS**](https://github.com/rustdesk/rustdesk/releases) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) + +## Dependencias + +Las versiones de escritorio utilizan Flutter o Sciter (obsoleto) para GUI, este tutorial es sólo para Sciter, ya que es más fácil y más amigable para empezar. Echa un vistazo a nuestro [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) para la construcción de la versión Flutter. + +Por favor descarga la librería dinámica de Sciter tú mismo. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Pasos para compilar desde el inicio + +- Prepara el entorno de desarrollo de Rust y el entorno de compilación de C++ y Rust. + +- Instala [vcpkg](https://github.com/microsoft/vcpkg), y configura la variable de entono `VCPKG_ROOT` correctamente. + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/Osx: vcpkg install libvpx libyuv opus aom + +- Corre `cargo run` + +## Como compilar en linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Instala vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Arregla libvpx (Para Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Compila + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone --recurse-submodules https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Como compilar con Docker + +Empieza clonando el repositorio y compilando el contenedor de docker: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +git submodule update --init --recursive +docker build -t "rustdesk-builder" . +``` + +Entonces, cada vez que necesites compilar la aplicación, ejecuta el siguiente comando: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Ten en cuenta que la primera compilación puede tardar más tiempo antes de que las dependencias se almacenen en la caché, las siguientes compilaciones serán más rápidas. Además, si necesitas especificar diferentes argumentos al comando de compilación, puedes hacerlo al final del comando en la posición ``. Por ejemplo, si deseas compilar una versión optimizada para publicación, deberas ejecutar el comando anterior seguido de `--release`. El ejecutable resultante estará disponible en la carpeta de destino en tu sistema, y puede ser ejecutado con: + +```sh +target/debug/rustdesk +``` + +O si estas ejecutando una versión para su publicación: + +```sh +target/release/rustdesk +``` + +Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del repositorio de RustDesk, de lo contrario la aplicación puede ser incapaz de encontrar los recursos necesarios. También ten en cuenta que otros subcomandos de cargo como `install` o `run` no estan actualmente soportados usando este metodo, ya que instalarían o ejecutarían el programa dentro del contenedor en lugar del host. + +## Estructura de archivos + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec de video, configuración, tcp/udp wrapper, protobuf, funciones para transferencia de archivos, y otras funciones de utilidad. +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de pantalla +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control del teclado/mouse especificos de cada plataforma +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: sonido/portapapeles/input/servicios de video, y conexiones de red +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar una conexión "peer to peer" +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed") +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter + +> [!Precaución] +> **Descargo de responsabilidad por uso indebido:**
+> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El uso indebido, como el acceso no autorizado, el control o la invasión de la privacidad, está estrictamente en contra de nuestras directrices. Los autores no son responsables de ningún uso indebido de la aplicación. + +## Capturas de pantalla + +![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) diff --git a/shelled/rustdesk-as-ref/docs/README-FA.md b/shelled/rustdesk-as-ref/docs/README-FA.md new file mode 100644 index 0000000..a0645e0 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-FA.md @@ -0,0 +1,159 @@ +

+ RustDesk - Your remote desktop
+ تصاویر محیط Ù†Ø±Ù…â€ŒØ§ÙØ²Ø§Ø± • + ساختار • + داکر • + ساخت • + سرور +

+

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

+

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

+ +با ما Ú¯ÙØªÚ¯Ùˆ کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [YouTube](https://www.youtube.com/@rustdesk) + + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%D9%88%DB%8C%DA%98%DA%AF%DB%8C%E2%80%8C%D9%87%D8%A7%DB%8C%20%D9%BE%DB%8C%D8%B4%D8%B1%D9%81%D8%AA%D9%87-blue)](https://rustdesk.com/pricing.html) + +راست‌دسک (RustDesk) Ù†Ø±Ù…â€ŒØ§ÙØ²Ø§Ø±ÛŒ برای کارکردن با رایانه‌ی رومیزی از راه دور است Ùˆ با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد Ùˆ شما را قادر Ù…ÛŒ سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید. + +می‌توانید از سرور rendezvous/relay ما Ø§Ø³ØªÙØ§Ø¯Ù‡ کنید، [سرور خودتان را راه‌اندازی کنید](https://rustdesk.com/server) یا +[ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk). + +ما از مشارکت همه استقبال Ù…ÛŒ کنیم. برای راهنمایی جهت مشارکت به[`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید. + +[راست‌دسک چطور کار Ù…ÛŒ کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) + +[Ø¯Ø±ÛŒØ§ÙØª Ù†Ø±Ù…â€ŒØ§ÙØ²Ø§Ø±](https://github.com/rustdesk/rustdesk/releases) + +## وابستگی ها + +نسخه‌های رومیزی از [sciter](https://sciter.com/) برای رابط کاربری گراÙیکی Ø§Ø³ØªÙØ§Ø¯Ù‡ می‌کنند. خواهشمندیم کتابخانه‌ی پویای sciter را خودتان دانلود کنید از این منابع Ø¯Ø±ÛŒØ§ÙØª کنید. + +- [ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) +- [لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) +- [Ù…Ú©](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +نسخه های همراه از Flutter Ø§Ø³ØªÙØ§Ø¯Ù‡ Ù…ÛŒ کنند. نسخه‌ی رومیزی را هم از Sciter به Flutter منتقل خواهیم کرد. + +## نیازمندی‌های ساخت + +- محیط توسعه نرم Ø§ÙØ²Ø§Ø± Rust Ùˆ محیط ساخت ++C خود را آماده کنید + +- نرم Ø§ÙØ²Ø§Ø± [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید Ùˆ متغیر `VCPKG_ROOT` را به درستی تنظیم کنید. +- بسته‌های vcpkg مورد نیاز را نصب کنید: + - ویندوز: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static` + - Ù…Ú© Ùˆ لینوکس: `vcpkg install libvpx libyuv opus aom` +- این دستور را اجرا کنید: `cargo run` + +## [ساخت](https://rustdesk.com/docs/en/dev/build/) + +## نحوه ساخت بر روی لینوکس + +### ساخت بر روی (Ubuntu 18 (Debian 10 + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### ساخت بر روی (Fedora 28 (CentOS 8 + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### ساخت بر روی (Arch (Manjaro + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### نرم Ø§ÙØ²Ø§Ø± vcpkg را نصب کنید + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Ø±ÙØ¹ ایراد libvpx (برای ÙØ¯ÙˆØ±Ø§) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### ساخت + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## نحوه ساخت با داکر + +این مخزن Git را Ø¯Ø±ÛŒØ§ÙØª کنید Ùˆ کانتینر را به روش زیر بسازید + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +سپس، هر بار Ú©Ù‡ نیاز به ساخت Ù†Ø±Ù…â€ŒØ§ÙØ²Ø§Ø± داشتید، دستور زیر را اجرا کنید: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +توجه داشته باشید Ú©Ù‡ نخستین ساخت ممکن است به دلیل محلی نبودن وابستگی‌ها بیشتر طول بکشد. اما Ø¯ÙØ¹Ø§Øª بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختل٠برای دستور ساخت دارید، Ù…ÛŒ توانید این کار را در انتهای دستور ساخت Ùˆ از طریق `` انجام دهید. به عنوان مثال، اگر Ù…ÛŒ خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید Ùˆ در انتها `release--` را اضاÙÙ‡ کنید. ÙØ§ÛŒÙ„ اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود Ùˆ Ù…ÛŒ تواند با دستور: + +```sh +target/debug/rustdesk +``` + +یا برای نسخه بهینه سازی شده دستور زیر را اجرا کنید: + +```sh +target/release/rustdesk +``` + +Ù„Ø·ÙØ§Ù‹ اطمینان حاصل کنید Ú©Ù‡ این دستورات را از پوشه مخزن RustDesk اجرا Ù…ÛŒ کنید، در غیر این صورت ممکن است برنامه نتواند منابع مورد نیاز را پیدا کند. همچنین توجه داشته باشید Ú©Ù‡ سایر دستورات ÙØ±Ø¹ÛŒ Cargo مانند `install` یا `run` در حال حاضر از طریق این روش پشتیبانی نمی شوند زیرا برنامه به جای سیستم عامل میزبان, در داخل کانتینر نصب Ùˆ اجرا میشود. + +## ساختار پوشه ها + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client + +## تصاویر محیط Ù†Ø±Ù…â€ŒØ§ÙØ²Ø§Ø± + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-FI.md b/shelled/rustdesk-as-ref/docs/README-FI.md new file mode 100644 index 0000000..4c16797 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-FI.md @@ -0,0 +1,148 @@ +

+ RustDesk - Etätyöpöytäsi
+ Palvelimet • + Rakenna • + Docker • + Rakenne • + Tilannevedos
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi +

+ +Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Edistyneet%20Ominaisuudet-blue)](https://rustdesk.com/pricing.html) + +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 [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) avun saamiseksi. + +[**BINAARILATAUS**](https://github.com/rustdesk/rustdesk/releases) + +## Riippuvuudet + +Desktop-versiot käyttävät [sciter](https://sciter.com/) graafisena käyttöliittymänä, lataa sciter-dynaaminen kirjasto itsellesi. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Rakentamisaskeleet harppoen + +- Valmistele Rust-kehitysympäristö ja C++-rakentamisympäristö + +- Asenna [vcpkg](https://github.com/microsoft/vcpkg), ja aseta `VCPKG_ROOT`-ympäristömuuttuja oikein + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- suorita `cargo run` + +## Kuinka rakentaa Linux:issa + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Asenna vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Korjaa libvpx (Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Rakenna + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Kuinka rakennetaan Dockerin kanssa + +Aloita kloonaamalla tietovarasto ja rakentamalla docker-säiliö: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +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 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 +``` + +Tai, jos olet suorittamassa jakeluversion suoritettavaa tiedostoa: + +```sh +target/release/rustdesk +``` + +Varmista, että suoritat näitä komentoja RustDesktop-tietovaraston juurihakemistossa, muutoin sovellus ei ehkä löydä vaadittuja resursseja. Huomaa myös, että muita cargo-alikomentoja kuten `install` tai `run` ei nykyisin tueta tässä menetelmässä, koska ne asentavat tai suorittavat ohjelman säiliön sisällä eikä isäntäohjelman sisällä. + +## Tiedostorakenne + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs-funktiot tiedostosiirtoon, ja jotkut muut apuohjelmafunktiot +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: näyttökaappaukset +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Graafinen käyttöliittymä +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code + +## Tilannekuvat + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-FR.md b/shelled/rustdesk-as-ref/docs/README-FR.md new file mode 100644 index 0000000..c2e2588 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-FR.md @@ -0,0 +1,152 @@ +

+ RustDesk - Your remote desktop
+ Serveurs - + Build - + Docker - + Structure - + Images
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle. +

+ +Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Fonctionnalit%C3%A9s%20Avanc%C3%A9es-blue)](https://rustdesk.com/pricing.html) + +Encore un autre logiciel de bureau à distance, écrit en Rust. Fonctionne directement, aucune configuration n'est nécessaire. Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, [configurer le vôtre](https://rustdesk.com/server), ou [écrire votre propre serveur de rendez-vous/relais](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk accueille les contributions de tout le monde. Voir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) pour plus d'informations. + +[**TÉLÉCHARGEMENT BINAIRE**](https://github.com/rustdesk/rustdesk/releases) + +## Dépendances + +Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface graphique, veuillez télécharger la bibliothèque dynamique sciter vous-même. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Étapes brutes de la compilation/build + +- Préparez votre environnement de développement Rust et votre environnement de compilation C++. + +- Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`. + + - Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/Osx : vcpkg install libvpx libyuv opus aom + +- Exécuter `cargo run` + +## Comment compiler/build sous Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Installer vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Corriger libvpx (Pour Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Construire + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +Exécution du cargo +``` + +## Comment construire avec Docker + +Commencez par cloner le dépôt et construire le conteneur Docker : + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Ensuite, chaque fois que vous devez compiler le logiciel, exécutez la commande suivante : + +```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 +``` + +Notez que la première compilation peut prendre plus de temps avant que les dépendances ne soient mises en cache, les compilations suivantes seront plus rapides. De plus, si vous devez spécifier différents arguments à la commande de compilation, vous pouvez le faire à la fin de la commande à la position ``. Par exemple, si vous voulez compiler une version de release optimisée, vous devez exécuter la commande ci-dessus suivie de `--release`. L'exécutable résultant sera disponible dans le dossier cible sur votre système, et peut être lancé avec : + +```sh +target/debug/rustdesk +``` + +Ou, si vous exécutez un exécutable provenant d'une release : + +```sh +target/release/rustdesk +``` + +Veuillez vous assurer que vous exécutez ces commandes à partir de la racine du dépôt RustDesk, sinon l'application ne pourra pas trouver les ressources requises. Notez également que les autres sous-commandes de cargo telles que `install` ou `run` ne sont pas actuellement supportées par cette méthode car elles installeraient ou exécuteraient le programme à l'intérieur du conteneur au lieu de l'hôte. + +## Structure du projet + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)** : codec vidéo, config, wrapper tcp/udp, protobuf, fonctions fs pour le transfert de fichiers, et quelques autres fonctions utilitaires. +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)** : capture d'écran +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)** : contrôle clavier/souris spécifique à la plate-forme +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)** : interface graphique +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)** : services audio/clipboard/input/vidéo, et connexions réseau +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)** : démarrer une connexion entre pairs +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)** : Communiquer avec [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attendre une connexion distante directe (TCP hole punching) ou relayée. +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)** : code spécifique à la plateforme + +> [!Attention] +> **Avertissement contre l'utilisation abusive:**
+> Les développeurs de RustDesk ne cautionnent ni ne soutiennent aucune utilisation non éthique ou illégale de ce logiciel. Toute utilisation abusive, telle que l'accès non autorisé, le contrôle ou l'invasion de la vie privée, est strictement contraire à nos directives. Les auteurs ne sont pas responsables de toute utilisation abusive de l'application. + +## Images + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-GR.md b/shelled/rustdesk-as-ref/docs/README-GR.md new file mode 100644 index 0000000..8b0276b --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-GR.md @@ -0,0 +1,171 @@ +

+ RustDesk - Your remote desktop
+ Διακομιστές • + Build • + Docker • + Δομή • + Στιγμιότυπα
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk]
+ ΧÏειαζόμαστε τη βοήθειά σας για να μεταφÏάσουμε αυτό το αÏχείο README, το RustDesk UI και το Doc στη μητÏική σας γλώσσα +

+ +Επικοινωνήστε μαζί μας μέσω: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%CE%A0%CF%81%CE%BF%CE%B7%CE%B3%CE%BC%CE%AD%CE%BD%CE%B5%CF%82%20%CE%94%CF%85%CE%BD%CE%B1%CF%84%CF%8C%CF%84%CE%B7%CF%84%CE%B5%CF%82-blue)](https://rustdesk.com/pricing.html) + +Ένα λογισμικό απομακÏυσμένης επιφάνειας εÏγασίας, γÏαμμένο σε γλώσσα Rust. Δεν χÏειάζεται κάποια παÏαμετÏοποίηση, λειτουÏγεί αμέσως μετά την εγκατάσταση. Έχετε τον πλήÏη έλεγχο των δεδομένων σας, χωÏίς να ανησυχείτε για την ασφάλειά τους. ΜποÏείτε να χÏησιμοποιήσετε τους Ï€ÏοκαθοÏισμένους διακομιστές rendezvous/αναμετάδοσης, [να εγκαταστήσετε τον δικό σας διακομιστή](https://rustdesk.com/server), ή [να αναπτÏξετε ένα δικό σας διακομιστή rendezvous/αναμετάδοσης](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +Το RustDesk ενθαÏÏÏνει τη συνεισφοÏά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε. + +[**Συχνές εÏωτήσεις**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**Κατεβάστε τα αÏχεία**](https://github.com/rustdesk/rustdesk/releases) + +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## ΠÏοαπαιτοÏμενα για build + +Στις παÏαθυÏικές εκδόσεις χÏησιμοποιείται είτε το [sciter](https://sciter.com/) είτε το Flutter, τα παÏακάτω βήματα είναι μόνο για το Sciter. + +ΠαÏακαλώ κατεβάστε μόνοι σας την δυναμική βιβλιοθήκη sciter. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Γενικά βήματα ώστε να κάνετε build + +- ΠÏοετοιμάστε τα πεÏιβάλλοντα Ï€ÏογÏÎ±Î¼Î¼Î±Ï„Î¹ÏƒÎ¼Î¿Ï Rust και C++ + +- Εγκαταστήσετε το [vcpkg](https://github.com/microsoft/vcpkg), και Ïυθμίστε σωστά την παÏάμετÏο συστήματος `VCPKG_ROOT` + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- Εκτελέστε `cargo run` + +## [Build](https://rustdesk.com/docs/en/dev/build/) + +## Πως να το κάνετε build στο Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +``` +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Εγκατάσταση vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### ΔιόÏθωση libvpx (για Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Build + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Πως να κάνετε build στο Docker + +Ξεκινήστε κλωνοποιώντας το αποθετήÏιο και κάνοντας build το docker container: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Στη συνέχεια, κάθε φοÏά που επιθυμείτε να κάνετε build την εφαÏμογή, εκτελέστε την ακόλουθη εντολή: + +```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 +``` + +Σημειώστε ότι το Ï€Ïώτο build μποÏεί να διαÏκέσει πεÏισσότεÏο, ώστε να αποθηκευτοÏν στην Ï€ÏοσωÏινή μνήμη οι εξαÏτήσεις, τα επόμενα build θα είναι ταχÏτεÏα. Επιπλέον, εάν Ï€Ïέπει να καθοÏίσετε διαφοÏετικές παÏαμέτÏους στην εντολή build, μποÏείτε να το κάνετε στο τέλος της εντολής με την χÏήση ``. Για παÏάδειγμα, εάν επιθυμείτε να δημιουÏγήσετε μια βελτιστοποιημένη έκδοση της εφαÏμογής, θα εκτελέσετε την παÏαπάνω εντολή ακολουθοÏμενη από το `--release`. Το εκτελέσιμο αÏχείο θα είναι διαθέσιμο στον Ï€ÏοκαθοÏισμένο φάκελο στο σÏστημά σας και μποÏεί να εκτελεστεί με: + +```sh +target/debug/rustdesk +``` + +Ή στην πεÏίπτωση μιας βελτιστοποιημένης έκδοσης της εφαÏμογής εκτελέστε: + +```sh +target/release/rustdesk +``` + +Βεβαιωθείτε ότι εκτελείτε αυτές τις εντολές από την αÏχική διαδÏομή του αποθετηÏίου του RustDesk, διαφοÏετικά η εφαÏμογή ενδέχεται να μην είναι σε θέση να βÏεί τους απαιτοÏμενους πόÏους. Σημειώστε επίσης ότι άλλες υποεντολές, όπως το `install` ή το `run` δεν υποστηÏίζονται επί του παÏόντος μέσω αυτής της μεθόδου καθώς θα εγκαταστήσουν ή θα εκτελέσουν το Ï€ÏόγÏαμμα εντός του container αντί του κεντÏÎ¹ÎºÎ¿Ï Ï…Ï€Î¿Î»Î¿Î³Î¹ÏƒÏ„Î®. + +## Δομή φακέλων + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client + +## Στιγμιότυπα + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-HU.md b/shelled/rustdesk-as-ref/docs/README-HU.md new file mode 100644 index 0000000..82d1d55 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-HU.md @@ -0,0 +1,163 @@ +

+ RustDesk - Your remote desktop
+ Szerverek • + Építés • + Docker • + Struktúra • + Képernyőképek
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Kell a segítséged, hogy lefordítsuk ezt a README-t, a RustDesk UI-t és a Dokumentációt az anyanyelvedre +

+ +Beszélgess velünk: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Speci%C3%A1lis%20Funkci%C3%B3k-blue)](https://rustdesk.com/pricing.html) + +A RustDesk egy távoli elérésű asztali szoftver, Rust-ban írva. Működik mindenféle konfiguráció nélkül, feltelepítéssel, vagy anélkül. Az adataidat teljesen te kezeled, nincs szükség aggódásra a harmadik felek miatt. Használhatod a RustDesk punblikus randevú/relay szervereit, [hostolhatsz sajátot](https://rustdesk.com/server), vagy akár [írhatsz is egyet](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lásd a [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) fájlt a kezdéshez. + +[**Hogyan működik a RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) + +[**BINARY LELTÖLTÉS**](https://github.com/rustdesk/rustdesk/releases) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Dependencies + +Az asztali verziók [sciter](https://sciter.com/)-t használnak a GUI-hoz, kérlek telepítsd a dynamikus könyvtárat magad. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +A telefonos verziók Flutter-t hasznának. Később lehetséges hogy Sciterről Flutterre migrálunk az asztali verziókban is. + +## Építési pontok + +- Készítsd elő a Rust, C++ fejlesztői környezetet (env) + +- Telepítsd a [vcpkg](https://github.com/microsoft/vcpkg)-t, és állítsd be a `VCPKG_ROOT` környezeti változót helyesen + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- Futtasd a `cargo run` parancsot + +## [Építés](https://rustdesk.com/docs/hu/dev/build/) + +## Hogyan építs Linuxon + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Telepítsd a vcpkg-t + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Fixeld a libvpx-t (Fedora-n csak) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Építés + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Hogyan építs Dockerrel + +Kezdjünk a repo clónozásával, majd pedig a Docker container megépítésével: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Ezután, minden egyes alkalommal amikor meg kell építened a RustDesk-et, futtasd a kövezkező parancsot: + +```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 +``` + +Fontos, hogy az első építés lehet hogy több ideig fog tartani mint a következőek, mivel a dependenciek még nincsenek cachelve. Emelett, ha esetleg szeretnél valamilyen argumentumot hozzáadni az építő parancshoz, akkor megteheted a paracssor végén, a `` argumentum használatával. Például ha egy optimalizált release éptést szeretnél megépíteni, akkor add hozzá a fenti parancsorhoz a `--release` opciót. A futtatható binary elérhető lesz a target mappában a rendszereden, futtatni a következőképpen tudod: + +```sh +target/debug/rustdesk +``` + +Vagy ha release binary, akkor: + +```sh +target/release/rustdesk +``` + +Kérlek mindenképpen nézd meg hogy ezeket a parancsokat a root RustDesk mappában futtatod e, különben a RustDesk lehet hogy nem fogja megtalálni az építéshez szükséges elemeket. Fontos az is, hogy jelenleg más cargo subparancsok, például `install`vagy `run` nem támogatottak, mivel egy Dockeres építés esetén elindítanák a programot a containeren belül. + + +## Fájl Struktúra + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client + +## Képernyőképek + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-ID.md b/shelled/rustdesk-as-ref/docs/README-ID.md new file mode 100644 index 0000000..7b63d0e --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-ID.md @@ -0,0 +1,166 @@ +

+ RustDesk - Your remote desktop
+ Servers • + Build • + Docker • + Structure • + Snapshot
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Kami membutuhkan bantuanmu untuk menterjemahkan file README dan RustDesk UI ke Bahasa Indonesia +

+ +Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Fitur%20Lanjutan-blue)](https://rustdesk.com/pricing.html) + +[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open) + +Merupakan perangkat lunak Remote Desktop yang baru, dan dibangun dengan Rust. Bahkan kamu bisa langsung menggunakannya tanpa perlu melakukan konfigurasi tambahan. Serta memiliki kontrol penuh terhadap semua data, tanpa perlu merasa was-was tentang isu keamanan, dan yang lebih menarik adalah memiliki opsi untuk menggunakan server rendezvous/relay milik kami, [konfigurasi server sendiri](https://rustdesk.com/server), atau [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTING-ID.md`](CONTRIBUTING-ID.md) untuk melihat panduan. + +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**UNDUH BINARY**](https://github.com/rustdesk/rustdesk/releases) + +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Dependensi + +Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter + +Kamu bisa mengunduh Sciter dynamic library disini. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Langkah awal untuk memulai + +- Siapkan env development Rust dan env build C++ + +- Install [vcpkg](https://github.com/microsoft/vcpkg), dan atur variabel env `VCPKG_ROOT` dengan benar + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- jalankan `cargo run` + +## [Build](https://rustdesk.com/docs/en/dev/build/) + +## Cara Build di Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Install vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Mengatasi masalah libvpx (Untuk Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Build + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Cara Build dengan Docker + +Mulailah dengan melakukan kloning (clone) repositori dan build dengan docker container: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Selanjutnya, setiap kali ketika kamu akan melakukan build aplikasi, jalankan perintah berikut: + +```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 +``` + +Perlu diingat bahwa pada saat build pertama kali, mungkin memerlukan waktu lebih lama sebelum dependensi di-cache, build berikutnya akan lebih cepat. Selain itu, jika perlu menentukan argumen yang berbeda untuk perintah build, kamu dapat melakukannya di akhir perintah di posisi ``. Misalnya, jika ingin membangun versi rilis yang dioptimalkan, jalankan perintah di atas dan tambahkan `--release`. Hasil eksekusi perintah tersebut akan tersimpan pada target folder di sistem kamu, dan dapat dijalankan dengan: + +```sh +target/debug/rustdesk +``` + +Atau, jika kamu menjalankan rilis yang dapat dieksekusi: + +```sh +target/release/rustdesk +``` + +Harap pastikan bahwa kamu menjalankan perintah ini dari repositori root RustDesk, jika tidak demikian, aplikasi mungkin tidak dapat menemukan sumber yang diperlukan. Dan juga, perintah cargo seperti `install` atau `run` saat ini tidak didukung melalui metode ini karena, proses menginstal atau menjalankan program terjadi di dalam container bukan pada host. + +## Struktur File + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions untuk transfer file, dan beberapa fungsi utilitas lainnya +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: spesifikasi platform keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, dan network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikasi dengan [rustdesk-server](https://github.com/rustdesk/rustdesk-server), menunggu untuk remote direct (TCP hole punching) atau relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: kode khusus platform + +## Snapshots + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-IT.md b/shelled/rustdesk-as-ref/docs/README-IT.md new file mode 100644 index 0000000..0393ee6 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-IT.md @@ -0,0 +1,179 @@ +

+ RustDesk - il tuo desktop remoto
+ Server • + Compilazione • + Docker • + Struttura • + Schermate
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
+ Abbiamo bisogno del tuo aiuto per tradurre questo file README e la UI RustDesk nella tua lingua nativa +

+ +Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Funzionalit%C3%A0%20Avanzate-blue)](https://rustdesk.com/pricing.html) + +[![Bounties aperti](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Frustdesk%2Fbounties%3Fstatus%3Dopen)](https://console.algora.io/org/rustdesk/bounties?status=open) + +Ancora un altro software per il controllo remoto del desktop, scritto in Rust. Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza. Puoi usare il nostro server rendezvous/relay, [configurare il tuo server](https://rustdesk.com/server) o [realizzare il tuo server rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come iniziare a contribuire, vedi [CONTRIBUTING.md](CONTRIBUTING-IT.md). + +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**SCARICA PROGRAMMA**](https://github.com/rustdesk/rustdesk/releases) + +[**SCARICA NIGHTLY**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Dipendenze + +Le versioni desktop utilizzano Flutter o Sciter (deprecato) per l'interfaccia utente, questo tutorial è solo per Sciter, poiché è più facile per iniziare. Controlla il nostro [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) per la compilazione della versione Flutter. + +Scarica la libreria dinamica Sciter. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Passaggi per la compilazione + +- Prepara l'ambiente per lo sviluppo e compilazione in Rust e C++ + +- Installa [vcpkg](https://github.com/microsoft/vcpkg), e imposta correttamente la variabile d'ambiente `VCPKG_ROOT` + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- Esegui `cargo run` + +## [Build](https://rustdesk.com/docs/en/dev/build/) + +## Come compilare in Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Installa vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Correzione libvpx (per Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Compilazione + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Come compilare con Docker + +Clona il repository e compila i container docker: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Quindi, ogni volta che devi compilare l'applicazione, esegui il seguente comando: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Tieni presente che la prima build potrebbe richiedere più tempo prima che le dipendenze vengano memorizzate nella cache, le build successive saranno più veloci. Inoltre, se hai bisogno di specificare argomenti diversi per il comando build, puoi farlo alla fine del comando nella posizione ``. Ad esempio, se vuoi creare una versione di rilascio ottimizzata, esegui il comando precedentemente indicato seguito da `--release`. L'eseguibile generato sarà creato nella cartella destinazione del sistema e può essere eseguito con: + +```sh +target/debug/rustdesk +``` + +Oppure, se stai avviando un eseguibile di rilascio: + +```sh +target/release/rustdesk +``` + +Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste. Nota inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host. + +## Struttura dei file + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funzioni per il trasferimento file, e altre funzioni utili. +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: cattura dello schermo +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controllo tastiera/mouse specifico della piattaforma +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementazione del copia e incolla dei file per Windows, Linux, macOS. +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Sciter UI obsoleto (deprecato) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servizi audio/appunti/input/video e connessioni di rete +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: avvio di una connessione peer +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed) +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: codice specifico della piattaforma +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter + +> [!Attenzione] +> **Dichiarazione di non responsabilità per uso improprio:**
+> Gli sviluppatori di RustDesk non approvano né supportano alcun uso non etico o illegale di questo software. L'uso improprio, come l'accesso non autorizzato, il controllo o l'invasione della privacy, è strettamente contro le nostre linee guida. Gli autori non sono responsabili per qualsiasi uso improprio dell'applicazione. + +## Schermate + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-JP.md b/shelled/rustdesk-as-ref/docs/README-JP.md new file mode 100644 index 0000000..c9f7564 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-JP.md @@ -0,0 +1,183 @@ +

+ RustDesk - ã‚ãªãŸã®ãŸã‚ã®ãƒªãƒ¢ãƒ¼ãƒˆãƒ‡ã‚¹ã‚¯ãƒˆãƒƒãƒ—
+ Servers • + Build • + Docker • + Structure • + Snapshot
+ [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
+ READMEã‚„RustDesk UI〠RustDesk Docã®ç¿»è¨³è€…を歓迎ã—ã¾ã™ï¼ +

+ +ç§ãŸã¡ã¨è©±ã™: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%E9%AB%98%E5%BA%A6%E3%81%AA%E6%A9%9F%E8%83%BD-blue)](https://rustdesk.com/pricing.html) + +Rustã§æ›¸ã‹ã‚ŒãŸã€è¨­å®šä¸è¦ã§ã™ãã«ä½¿ãˆã‚‹ãƒªãƒ¢ãƒ¼ãƒˆãƒ‡ã‚¹ã‚¯ãƒˆãƒƒãƒ—ソフトウェアã§ã™ã€‚自分ã®ãƒ‡ãƒ¼ã‚¿ã‚’完全ã«ã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ«ã§ãã€ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ã®å¿ƒé…ã‚‚ã‚りã¾ã›ã‚“。ç§ãŸã¡ã®ãƒ©ãƒ³ãƒ‡ãƒ–ー/リレーサーãƒã‚’使ã†ã“ã¨ã‚‚ã€[自分ã§ã‚µãƒ¼ãƒãƒ¼ã‚’セットアップã™ã‚‹](https://rustdesk.com/server) ã“ã¨ã‚‚〠[自分ã§ãƒ©ãƒ³ãƒ‡ãƒ–ー/リレーサーãƒã‚’作æˆã™ã‚‹](https://github.com/rustdesk/rustdesk-server-demo)ã“ã¨ã‚‚ã§ãã¾ã™ã€‚ + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDeskã¯çš†ã•ã‚“ã®è²¢çŒ®ã‚’歓迎ã—ã¾ã™ã€‚ +è²¢çŒ®ã®æ–¹æ³•ã«ã¤ã„ã¦ã¯[CONTRIBUTING.md](CONTRIBUTING.md)ã‚’ã”確èªãã ã•ã„。 + +[**よãã‚る質å•**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**パッケージã®ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰**](https://github.com/rustdesk/rustdesk/releases) + +[**ナイトリービルド**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[F-Droidã§å…¥æ‰‹ã™ã‚‹](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## ä¾å­˜é–¢ä¿‚ + +デスクトップ版ã§ã¯GUIã«Flutterã¾ãŸã¯Sciter(éžæŽ¨å¥¨)を使用ã—ã¾ã™ãŒã€ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã§ã¯åˆ†ã‹ã‚Šã‚„ã™ãã€ç°¡å˜ãªSciterã®ã¿ã‚’対象ã«è§£èª¬ã—ã¦ã„ã¾ã™ã€‚Flutterã§ã®ãƒ“ルド方法ã«ã¤ã„ã¦ã¯[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)ã‚’ã”覧ãã ã•ã„。 + +Sciter dynamic libraryを事å‰ã«ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰ã—ã¦ãã ã•ã„。 + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## ビルド手順 + +- Rust開発環境ã¨C++ビルド環境を準備ã—ã¾ã™ã€‚ + +- [vcpkg](https://github.com/microsoft/vcpkg)をインストールã—ã€ç’°å¢ƒå¤‰æ•°ã«`VCPKG_ROOT`を設定ã—ã¾ã™ã€‚ +ãã®å¾Œã€ä»¥ä¸‹ã®ã‚³ãƒžãƒ³ãƒ‰ã‚’実行ã—ã¾ã™ã€‚ + + - Windowsã®å ´åˆ: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/macOSã®å ´åˆ: vcpkg install libvpx libyuv opus aom + +- `cargo run`を実行ã—ã¾ã™ã€‚ + +## [ビルド](https://rustdesk.com/docs/en/dev/build/) + +## Linuxã§ã®ãƒ“ルド方法 + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### vcpkgã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ« + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### libvpxã®ä¿®æ­£ (Fedoraã®ã¿) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### ビルド + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Dockerã§ã®ãƒ“ルド方法 + +リãƒã‚¸ãƒˆãƒªã‚’クローンã—ã€Dockerコンテナを構築ã—ã¾ã™: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +以下ã®ã‚³ãƒžãƒ³ãƒ‰ã‚’実行ã—ã¾ã™: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` +ã“ã®ã‚³ãƒžãƒ³ãƒ‰ã¯RustDeskをビルドã™ã‚‹åº¦ã«å®Ÿè¡Œã™ã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚ + +åˆå›žãƒ“ãƒ«ãƒ‰ã¯æ™‚é–“ãŒã‹ã‹ã‚‹ã‹ã‚‚ã—れã¾ã›ã‚“ãŒã€2回目以é™ã¯ä¾å­˜é–¢ä¿‚ãŒã‚­ãƒ£ãƒƒã‚·ãƒ¥ã•れるãŸã‚ã€ãƒ“ルドã«ã‹ã‹ã‚‹æ™‚é–“ãŒçŸ­ããªã‚Šã¾ã™ã€‚ +ビルドコマンドã«è¿½åŠ ã®å¼•数を指定ã™ã‚‹å¿…è¦ãŒã‚ã‚‹å ´åˆã¯ã€ã‚³ãƒžãƒ³ãƒ‰ã®æœ€å¾Œ(``ã®ä½ç½®)ã§æŒ‡å®šã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚例ãˆã°ã€æœ€é©åŒ–ã•れãŸãƒªãƒªãƒ¼ã‚¹ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’ビルドã—ãŸã„å ´åˆã¯ã€ä¸Šè¨˜ã®ã‚³ãƒžãƒ³ãƒ‰ã®å¾Œã« `--release` を追記ã—実行ã—ã¾ã™ã€‚ビルドã•れãŸå®Ÿè¡Œãƒ•ァイルã¯ã‚ãªãŸã®ã‚·ã‚¹ãƒ†ãƒ ã®ã‚¿ãƒ¼ã‚²ãƒƒãƒˆãƒ•ォルダã«ä¿å­˜ã•れã€ä¸‹è¨˜ã®ã‚³ãƒžãƒ³ãƒ‰ã§å®Ÿè¡Œã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + +デãƒãƒƒã‚°ãƒ“ルドを起動ã™ã‚‹å ´åˆ: +```sh +target/debug/rustdesk +``` + +リリースビルドを起動ã™ã‚‹å ´åˆ: + +```sh +target/release/rustdesk +``` + +コマンドをRustDeskリãƒã‚¸ãƒˆãƒªã®ãƒ«ãƒ¼ãƒˆã‹ã‚‰å®Ÿè¡Œã—ã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。ã¾ãŸã€`install` ã‚„ `run` ãªã©ã®ä»–ã®cargoサブコマンドã¯ã€ãƒ›ã‚¹ãƒˆã§ã¯ãªãコンテナ内ã§ãƒ—ログラムをインストールã€å®Ÿè¡Œã™ã‚‹ãŸã‚ã€ç¾åœ¨ã®æ–¹æ³•ã§ã¯ã‚µãƒãƒ¼ãƒˆã•れã¦ã„ã¾ã›ã‚“。 + +## ファイル構造 + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデックã€è¨­å®šã€tcp/udpラッパーã€protobufã€ãƒ•ァイル転é€ã«åˆ©ç”¨ã•れるfs関数やãã®ä»–ã®ãƒ¦ãƒ¼ãƒ†ã‚£ãƒªãƒ†ã‚£é–¢æ•° +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプãƒãƒ£ +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: プラットフォーム固有ã®ã‚­ãƒ¼ãƒœãƒ¼ãƒ‰/マウスæ“作 +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windowsã€Linuxã€macOSå‘ã‘ã®ãƒ•ァイルã®ã‚³ãƒ”ーã¨è²¼ã‚Šä»˜ã‘ã®å®Ÿè£… +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 廃止ã•れ㟠Sciter UI (éžæŽ¨å¥¨) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: +オーディオ/クリップボード/入力/ビデオ サービスã¨ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯æŽ¥ç¶š +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: ピア接続ã®é–‹å§‹ +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)ã¨é€šä¿¡ã—ã€ãƒªãƒ¢ãƒ¼ãƒˆã®ç›´æŽ¥æŽ¥ç¶š(TCPホールパンãƒãƒ³ã‚°)や中継接続を担ã†ã€‚ +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有ã®ã‚³ãƒ¼ãƒ‰ +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップã¨ãƒ¢ãƒã‚¤ãƒ«å‘ã‘ã®Flutterコード +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアントå‘ã‘ã®JavaScript + +> [!注æ„] +> **:䏿­£ä½¿ç”¨ã«é–¢ã™ã‚‹å…責事項**
+> RustDeskã®é–‹ç™ºè€…ã¯ã€ã“ã®ã‚½ãƒ•トウェアã®éžå€«ç†çš„ã¾ãŸã¯é•法ãªä½¿ç”¨ã‚’容èªã¾ãŸã¯æ”¯æŒã—ã¾ã›ã‚“ã€‚ä¸æ­£ã‚¢ã‚¯ã‚»ã‚¹ã€ä¸æ­£ãªåˆ¶å¾¡ã€ã¾ãŸã¯ãƒ—ライãƒã‚·ãƒ¼ã®ä¾µå®³ãªã©ã®ä¸æ­£ä½¿ç”¨ã¯ã€å½“社ã®ã‚¬ã‚¤ãƒ‰ãƒ©ã‚¤ãƒ³ã«å޳坆ã«é•åã—ã¾ã™ã€‚開発者ã¯ã€ã‚¢ãƒ—リケーションã®ä¸æ­£ä½¿ç”¨ã«å¯¾ã—ã¦ä¸€åˆ‡ã®è²¬ä»»ã‚’è² ã„ã¾ã›ã‚“。 + +## スクリーンショット + +![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) diff --git a/shelled/rustdesk-as-ref/docs/README-KR.md b/shelled/rustdesk-as-ref/docs/README-KR.md new file mode 100644 index 0000000..c301fde --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-KR.md @@ -0,0 +1,182 @@ +

+ RustDesk - Your remote desktop
+ 빌드 • + Docker • + 구조 • + 스냇샷
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk]
+ ì´ README, RustDesk UI ë° RustDesk 문서를 ê·€í•˜ì˜ ëª¨êµ­ì–´ë¡œ 번역하는 ë° ë„ì›€ì´ í•„ìš”í•©ë‹ˆë‹¤ +

+ +> [!Caution] +> **오용 면책 조항:**
+> RustDeskì˜ ê°œë°œìžëŠ” ì´ ì†Œí”„íŠ¸ì›¨ì–´ì˜ ë¹„ìœ¤ë¦¬ì  ë˜ëŠ” 불법ì ì¸ ì‚¬ìš©ì„ ë¬µì¸í•˜ê±°ë‚˜ ì§€ì›í•˜ì§€ 않습니다. 무단 액세스, 제어 ë˜ëŠ” ê°œì¸ì •ë³´ 침해와 ê°™ì€ ì˜¤ìš©ì€ ì—„ê²©í•˜ê²Œ ë‹¹ì‚¬ì˜ ì§€ì¹¨ì— ìœ„ë°°ë©ë‹ˆë‹¤. 작성ìžëŠ” ì‘ìš© í”„ë¡œê·¸ëž¨ì˜ ì˜¤ìš©ì— ëŒ€í•´ ì±…ìž„ì„ ì§€ì§€ 않습니다. + + +우리와 채팅: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%EA%B3%A0%EA%B8%89%20%EA%B8%B0%EB%8A%A5-blue)](https://rustdesk.com/pricing.html) + +ë˜ í•˜ë‚˜ì˜ ì›ê²© ë°ìФí¬í†± 솔루션으로, Rust로 작성ë˜ì—ˆìŠµë‹ˆë‹¤. 별ë„ì˜ ì„¤ì • ì—†ì´ ë°”ë¡œ 사용할 수 있습니다. ë°ì´í„°ì— 대한 완전한 í†µì œê¶Œì„ ê°€ì§€ë©° ë³´ì•ˆì— ëŒ€í•œ ê±±ì •ì´ ì—†ìŠµë‹ˆë‹¤. ì €í¬ ëž‘ë°ë¶€/ë¦´ë ˆì´ ì„œë²„ë¥¼ 사용하거나, [ì§ì ‘ 설정](https://rustdesk.com/server)하거나, [ìžì‹ ë§Œì˜ ëž‘ë°ë¶€/ë¦´ë ˆì´ ì„œë²„ë¥¼ 작성](https://github.com/rustdesk/rustdesk-server-demo)í•  수 있습니다. + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk는 모든 ë¶„ë“¤ì˜ ê¸°ì—¬ë¥¼ 환ì˜í•©ë‹ˆë‹¤. 시작하는 ë° ë„ì›€ì´ í•„ìš”í•˜ë©´ [CONTRIBUTING-KR.md](CONTRIBUTING-KR.md)를 참조하세요. + +[**ìžì£¼ 묻는 질문**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**ë°”ì´ë„ˆë¦¬ 다운로드**](https://github.com/rustdesk/rustdesk/releases) + +[**ê°œë°œìž ë¹Œë“œ**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) + +## 종ì†ì„± + +ë°ìФí¬í†± ë²„ì „ì€ GUI로 Flutter ë˜ëŠ” Sciter (ë” ì´ìƒ ì§€ì›ë˜ì§€ 않ìŒ)를 사용하며, ì´ ìžìŠµì„œëŠ” 시작하기 ë” ì‰½ê³  친숙한 Sciter 전용입니다. Flutter 버전 빌드는 [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)ì„ í™•ì¸í•˜ì„¸ìš”. + +Sciter ë™ì  ë¼ì´ë¸ŒëŸ¬ë¦¬ë¥¼ ì§ì ‘ 다운로드하세요. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## 빌드를 위한 ì›ì‹œ 단계 + +- Rust 개발 환경과 C++ 빌드 í™˜ê²½ì„ ì¤€ë¹„í•©ë‹ˆë‹¤ + +- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경 변수를 올바르게 설정합니다 + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/macOS: vcpkg install libvpx libyuv opus aom + +- `cargo run` 실행 + +## [빌드](https://rustdesk.com/docs/en/dev/build/) + +## Linuxì—서 빌드하는 방법 + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### vcpkg 설치 + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### libvpx 수정 (Fedoraìš©) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### 빌드 + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone --recurse-submodules https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Docker로 빌드하는 방법 + +먼저 리í¬ì§€í† ë¦¬ë¥¼ 복제하고 Docker 컨테ì´ë„ˆë¥¼ 빌드합니다: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +git submodule update --init --recursive +docker build -t "rustdesk-builder" . +``` + +그런 ë‹¤ìŒ ì‘ìš© í”„ë¡œê·¸ëž¨ì„ ë¹Œë“œí•´ì•¼ í•  때마다 ë‹¤ìŒ ëª…ë ¹ì„ ì‹¤í–‰í•©ë‹ˆë‹¤: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +첫 번째 빌드는 종ì†ì„±ì´ ìºì‹œë˜ê¸°ê¹Œì§€ ì‹œê°„ì´ ì˜¤ëž˜ 걸릴 수 있으며, ì´í›„ 빌드는 ë” ë¹¨ë¼ì§‘니다. ë˜í•œ 빌드 ëª…ë ¹ì— ë‹¤ë¥¸ ì¸ìˆ˜ë¥¼ 지정해야 하는 경우 명령 ëì˜ `` ìœ„ì¹˜ì— ì¸ìˆ˜ë¥¼ 지정할 수 있습니다. 예를 들어 최ì í™”ëœ ë¦´ë¦¬ìŠ¤ ë²„ì „ì„ ë¹Œë“œí•˜ë ¤ë©´ ìœ„ì˜ ëª…ë ¹ ë’¤ì— `--release`를 추가하면 ë©ë‹ˆë‹¤. ê²°ê³¼ 실행 파ì¼ì€ ì‹œìŠ¤í…œì˜ ëŒ€ìƒ í´ë”ì—서 사용할 수 있으며 실행할 수 있습니다:: + +```sh +target/debug/rustdesk +``` + +ë˜ëŠ” 릴리스 실행 파ì¼ì„ 실행하는 경우: + +```sh +target/release/rustdesk +``` + +RustDesk 리í¬ì§€í† ë¦¬ì˜ 루트ì—서 ì´ëŸ¬í•œ ëª…ë ¹ì„ ì‹¤í–‰í•˜ê³  있는지 확ì¸í•˜ì„¸ìš”. 그렇지 않으면 ì‘ìš© í”„ë¡œê·¸ëž¨ì´ í•„ìš”í•œ 리소스를 찾지 못할 수 있습니다. ë˜í•œ `install` ë˜ëŠ” `run` ê³¼ ê°™ì€ ë‹¤ë¥¸ cargo 하위 ëª…ë ¹ì€ í˜¸ìŠ¤íŠ¸ê°€ 아닌 컨테ì´ë„ˆ ë‚´ë¶€ì— í”„ë¡œê·¸ëž¨ì„ ì„¤ì¹˜í•˜ê±°ë‚˜ 실행하므로 현재 ì´ ë°©ë²•ì„ í†µí•´ ì§€ì›ë˜ì§€ 않는다는 ì ì— 유ì˜í•˜ì„¸ìš”. + +## íŒŒì¼ êµ¬ì¡° + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 ì½”ë±, 구성, tcp/udp wrapper, protobuf, íŒŒì¼ ì „ì†¡ì„ ìœ„í•œ fs 함수 ë° ê¸°íƒ€ 유틸리티 함수 +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 ìº¡ì³ +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 플랫í¼ë³„ 키보드/마우스 제어 +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows, Linux, macOSìš© íŒŒì¼ ë³µì‚¬ ë° ë¶™ì—¬ë„£ê¸° 구현 +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: ë” ì´ìƒ 사용ë˜ì§€ 않는 Sciter UI (ì§€ì› ì¤‘ë‹¨) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오/í´ë¦½ë³´ë“œ/ìž…ë ¥/비디오 서비스 ë° ë„¤íŠ¸ì›Œí¬ ì—°ê²° +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 ì—°ê²° 시작 +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, ì›ê²© 다ì´ë ‰íЏ (TCP 홀 펀칭) ë˜ëŠ” ë¦´ë ˆì´ ì—°ê²° 대기 +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫í¼ë³„ 코드 +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: ë°ìФí¬í†± ë° ëª¨ë°”ì¼ìš© Flutter 코드 +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter 웹 í´ë¼ì´ì–¸íŠ¸ìš© JavaScript + +## 스í¬ë¦°ìƒ· + +![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) + diff --git a/shelled/rustdesk-as-ref/docs/README-ML.md b/shelled/rustdesk-as-ref/docs/README-ML.md new file mode 100644 index 0000000..225d7b9 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-ML.md @@ -0,0 +1,148 @@ +

+ RustDesk - Your remote desktop
+ Servers • + Build • + Docker • + Structure • + Snapshot
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ à´ˆ README നിങàµà´™à´³àµà´Ÿàµ† മാതൃഭാഷയിലേകàµà´•ൠവിവർതàµà´¤à´¨à´‚ ചെയàµà´¯à´¾àµ» à´žà´™àµà´™àµ¾à´•àµà´•ൠനിങàµà´™à´³àµà´Ÿàµ† സഹായം ആവശàµà´¯à´®à´¾à´£àµ +

+ +à´žà´™àµà´™à´³àµà´®à´¾à´¯à´¿ ചാറàµà´±àµ ചെയàµà´¯àµà´•: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%E0%B4%B5%E0%B4%BF%E0%B4%95%E0%B4%B8%E0%B4%BF%E0%B4%A4%20%E0%B4%B8%E0%B4%B5%E0%B4%BF%E0%B4%B6%E0%B5%87%E0%B4%B7%E0%B4%A4%E0%B4%95%E0%B5%BE-blue)](https://rustdesk.com/pricing.html) + +റസàµà´±àµà´±à´¿àµ½ à´Žà´´àµà´¤à´¿à´¯ മറàµà´±àµŠà´°àµ റിമോടàµà´Ÿàµ ഡെസàµà´•àµà´Ÿàµ‹à´ªàµà´ªàµ സോഫàµà´±àµà´±àµâ€Œà´µàµ†à´¯àµ¼. ബോകàµâ€Œà´¸à´¿à´¨àµ à´ªàµà´±à´¤àµà´¤àµ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¿à´•àµà´•àµà´¨àµà´¨àµ, കോൺഫിഗറേഷൻ ആവശàµà´¯à´®à´¿à´²àµà´². à´¸àµà´°à´•àµà´·à´¯àµ†à´•àµà´•àµà´±à´¿à´šàµà´šàµ ആശങàµà´•കളൊനàµà´¨àµà´®à´¿à´²àµà´²à´¾à´¤àµ†, നിങàµà´™à´³àµà´Ÿàµ† ഡാറàµà´±à´¯àµà´Ÿàµ† പൂർണàµà´£ നിയനàµà´¤àµà´°à´£à´‚ നിങàµà´™àµ¾à´•àµà´•àµà´£àµà´Ÿàµ. നിങàµà´™àµ¾à´•àµà´•ൠഞങàµà´™à´³àµà´Ÿàµ† rendezvous/relay സെർവർ ഉപയോഗികàµà´•ാം, [à´¸àµà´µà´¨àµà´¤à´®à´¾à´¯à´¿ സജàµà´œàµ€à´•à´°à´¿à´•àµà´•àµà´•](https://rustdesk.com/server), à´…à´²àµà´²àµ†à´™àµà´•ിൽ [നിങàµà´™à´³àµà´Ÿàµ† à´¸àµà´µà´¨àµà´¤à´‚ rendezvous/relay സെർവർ à´Žà´´àµà´¤àµà´•](https://github.com/rustdesk/rustdesk-server-demo). + +à´Žà´²àµà´²à´¾à´µà´°àµà´Ÿàµ†à´¯àµà´‚ സംഭാവനയെ RustDesk à´¸àµà´µà´¾à´—തം ചെയàµà´¯àµà´¨àµà´¨àµ. ആരംഭികàµà´•àµà´¨àµà´¨à´¤à´¿à´¨àµà´³àµà´³ സഹായതàµà´¤à´¿à´¨àµ [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) കാണàµà´•. + +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +## ഡിപെൻഡൻസികൾ + +ഡെസàµâ€Œà´•àµâ€Œà´Ÿàµ‹à´ªàµà´ªàµ പതിപàµà´ªàµà´•ൾ GUI-à´¯àµâ€Œà´•àµà´•ായി [sciter](https://sciter.com/) ഉപയോഗികàµà´•àµà´¨àµà´¨àµ, ദയവായി à´¸àµâ€Œà´¸àµˆà´±àµà´±àµ¼ ഡൈനാമികൠലൈബàµà´°à´±à´¿ à´¸àµà´µà´¯à´‚ ഡൗൺലോഡൠചെയàµà´¯àµà´•. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## നിർമàµà´®à´¿à´•àµà´•ാനàµà´³àµà´³ അസംസàµà´•ൃത പടികൾ + +- നിങàµà´™à´³àµà´Ÿàµ† Rust development envà´¯àµà´‚ and C++ build envà´¯àµà´‚ തയàµà´¯à´¾à´±à´¾à´•àµà´•àµà´• + +- [vcpkg](https://github.com/microsoft/vcpkg) ഇൻസàµà´±àµà´±à´¾àµ¾ ചെയàµà´¤àµ `VCPKG_ROOT` env വേരിയബിൾ ശരിയായി സജàµà´œà´®à´¾à´•àµà´•àµà´• + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- run `cargo run` + +## ലിനകàµà´¸à´¿àµ½ à´Žà´™àµà´™à´¨àµ† നിർമàµà´®à´¿à´•àµà´•ാം + +### ഉബàµà´£àµà´Ÿàµ 18 (ഡെബിയൻ 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### ഫെഡോറ 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### ആർചൠ(മഞàµà´šà´¾à´°àµ‹) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### vcpkg ഇൻസàµà´±àµà´±à´¾àµ¾ ചെയàµà´¯àµà´• + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### libvpx പരിഹരികàµà´•àµà´• (ഫെഡോറയàµà´•àµà´•àµ) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### നിർമാണം + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## ഡോകàµà´•ർ ഉപയോഗിചàµà´šàµ à´Žà´™àµà´™à´¨àµ† നിർമàµà´®à´¿à´•àµà´•ാം + + റെപàµà´ªàµ‹à´¸à´¿à´±àµà´±àµ‹à´±à´¿ à´•àµà´²àµ‹à´£àµà´šàµ†à´¯àµâ€Œà´¤àµ ഡോകàµà´•ർ à´•à´£àµà´Ÿàµ†à´¯àµâ€Œà´¨àµ¼ നിർമàµà´®à´¿à´•àµà´•àµà´¨àµà´¨à´¤à´¿à´²àµ‚ടെ ആരംഭികàµà´•àµà´•: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +à´¤àµà´Ÿàµ¼à´¨àµà´¨àµ, ഓരോ തവണയàµà´‚ നിങàµà´™àµ¾ ആപàµà´²à´¿à´•àµà´•േഷൻ നിർമàµà´®à´¿à´•àµà´•േണàµà´Ÿà´¤àµà´£àµà´Ÿàµ, ഇനിപàµà´ªà´±à´¯àµà´¨àµà´¨ കമാൻഡൠപàµà´°à´µàµ¼à´¤àµà´¤à´¿à´ªàµà´ªà´¿à´•àµà´•àµà´•: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +ഡിപൻഡൻസികൾ കാഷെ ചെയàµà´¯àµà´¨àµà´¨à´¤à´¿à´¨àµà´®àµà´®àµà´ªàµ ആദàµà´¯ ബിൽഡൠകൂടàµà´¤àµ½ സമയമെടàµà´¤àµà´¤àµ‡à´•àµà´•ാം, à´¤àµà´Ÿàµ¼à´¨àµà´¨àµà´³àµà´³ ബിൽഡàµà´•ൾ വേഗതàµà´¤à´¿à´²à´¾à´•àµà´‚. കൂടാതെ, നിങàµà´™àµ¾à´•àµà´•ൠബിൽഡൠകമാൻഡിലേകàµà´•ൠവàµà´¯à´¤àµà´¯à´¸àµà´¤ ആർഗàµà´¯àµà´®àµ†à´¨àµà´±àµà´•ൾ à´µàµà´¯à´•àµà´¤à´®à´¾à´•àµà´•ണമെങàµà´•ിൽ, കമാൻഡിനàµà´±àµ† അവസാനം `` à´¸àµà´¥à´¾à´¨à´¤àµà´¤àµ നിങàµà´™àµ¾à´•àµà´•ൠഅങàµà´™à´¨àµ† ചെയàµà´¯à´¾à´‚. ഉദാഹരണതàµà´¤à´¿à´¨àµ, നിങàµà´™àµ¾ ഒരൠഒപàµà´±àµà´±à´¿à´®àµˆà´¸àµ ചെയàµà´¤ റിലീസൠപതിപàµà´ªàµ നിർമàµà´®à´¿à´•àµà´•ാൻ ആഗàµà´°à´¹à´¿à´•àµà´•àµà´¨àµà´¨àµà´µàµ†à´™àµà´•ിൽ, à´®àµà´•ളിലàµà´³àµà´³ കമാൻഡൠതàµà´Ÿàµ¼à´¨àµà´¨àµ `--release` നിങàµà´™àµ¾ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¿à´ªàµà´ªà´¿à´•àµà´•àµà´‚. തതàµà´«à´²à´®à´¾à´¯àµà´£àµà´Ÿà´¾à´•àµà´¨àµà´¨ à´Žà´•àµà´¸à´¿à´•àµà´¯àµ‚à´Ÿàµà´Ÿà´¬à´¿àµ¾ നിങàµà´™à´³àµà´Ÿàµ† സിസàµà´±àµà´±à´¤àµà´¤à´¿à´²àµ† ടാർഗെറàµà´±àµ ഫോൾഡറിൽ ലഭàµà´¯à´®à´¾à´•àµà´‚, കൂടാതെ ഇതൠഉപയോഗിചàµà´šàµ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¿à´ªàµà´ªà´¿à´•àµà´•ാം: + +```sh +target/debug/rustdesk +``` + +à´…à´²àµà´²àµ†à´™àµà´•ിൽ, നിങàµà´™àµ¾ ഒരൠറിലീസൠഎകàµà´¸à´¿à´•àµà´¯àµ‚à´Ÿàµà´Ÿà´¬à´¿àµ¾ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¿à´ªàµà´ªà´¿à´•àµà´•àµà´•യാണെങàµà´•ിൽ: + +```sh +target/release/rustdesk +``` + +RustDesk റിപàµà´ªàµ‹à´¸à´¿à´±àµà´±à´±à´¿à´¯àµà´Ÿàµ† റൂടàµà´Ÿà´¿àµ½ നിനàµà´¨à´¾à´£àµ നിങàµà´™àµ¾ à´ˆ കമാൻഡàµà´•ൾ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¿à´ªàµà´ªà´¿à´•àµà´•àµà´¨àµà´¨à´¤àµ†à´¨àµà´¨àµ ദയവായി ഉറപàµà´ªà´¾à´•àµà´•àµà´•, à´…à´²àµà´²à´¾à´¤àµà´¤à´ªà´•àµà´·à´‚ ആപàµà´²à´¿à´•àµà´•േഷനൠആവശàµà´¯à´®à´¾à´¯ ഉറവിടങàµà´™àµ¾ à´•à´£àµà´Ÿàµ†à´¤àµà´¤à´¾àµ» à´•à´´à´¿à´žàµà´žàµ‡à´•àµà´•à´¿à´²àµà´². ഹോസàµà´±àµà´±à´¿à´¨àµ പകരം à´•à´£àµà´Ÿàµ†à´¯àµâ€Œà´¨à´±à´¿à´¨àµà´³àµà´³à´¿àµ½ à´ªàµà´°àµ‹à´—àµà´°à´¾à´‚ ഇൻസàµà´±àµà´±à´¾àµ¾ ചെയàµà´¯àµà´•യോ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¿à´ªàµà´ªà´¿à´•àµà´•àµà´•യോ ചെയàµà´¯àµà´¨àµà´¨à´¤à´¿à´¨à´¾àµ½, `install` à´…à´²àµà´²àµ†à´™àµà´•ിൽ `run` പോലàµà´³àµà´³ മറàµà´±àµ കാർഗോ സബàµâ€Œà´•മാൻഡàµà´•ൾ നിലവിൽ à´ˆ രീതിയെ പിനàµà´¤àµà´£à´¯àµà´•àµà´•àµà´¨àµà´¨à´¿à´²àµà´² à´Žà´¨àµà´¨à´¤àµà´‚ à´¶àµà´°à´¦àµà´§à´¿à´•àµà´•àµà´•. + +## ഫയൽ ഘടന + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code + +## à´¸àµà´¨à´¾à´ªàµà´ªàµà´·àµ‹à´Ÿàµà´Ÿàµà´•ൾ + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-NL.md b/shelled/rustdesk-as-ref/docs/README-NL.md new file mode 100644 index 0000000..45d68b2 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-NL.md @@ -0,0 +1,168 @@ +

+ RustDesk - Uw bureaublad op afstand
+ Servers • + Bouwen • + Docker • + Structuur • + Snapshot
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Wij hebben uw hulp nodig om dit README bestand te vertalen, RustDesk UI en Doc naar uw moedertaal +

+ +Chat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Geavanceerde%20Functies-blue)](https://rustdesk.com/pricing.html) + +Alweer een andere programma voor -bureaublad op afstand-, geschreven in Rust. Werkt -out of the box-, geen configuratie nodig. U heeft volledige controle over uw gegevens, en hoeft zich geen zorgen te maken over de beveiliging. U kunt onze rendez-vous/relay server gebruiken, [je eigen server opzetten](https://rustdesk.com/blog/id-relay-set), of [je eigen rendez-vous/relay-server schrijven](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk verwelkomt bijdragen van iedereen. Zie [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) voor hulp om aan de slag te gaan. + +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) (meest recente build) + +[Download het op F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Afhankelijkheden + +Desktop versies gebruiken [sciter](https://sciter.com/) of Flutter voor GUI, deze handleiding is alleen voor Sciter. + +Download zelf de dynamic library van Sciter. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Ruwe stappen om te bouwen + +- Bereid je Rust-ontwikkelomgeving en C++-bouwomgeving voor. + +- Installeer [vcpkg](https://github.com/microsoft/vcpkg) en configureer de `VCPKG_ROOT` omgevingsvariabele op de juiste manier: + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- Voer uit: `cargo run` + +## [Bouwen](https://rustdesk.com/docs/en/dev/build/) + +## Bouwen op Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Installatie van vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Fix voor libvpx (voor Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Bouwen + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Bouwen met Docker + +Begin met het klonen van de repository en het bouwen van de docker container: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Elke keer dat u de toepassing moet bouwen, voert u het volgende commando uit: + +```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 +``` + +Let op dat de eerste build langer kan duren omdat de dependencies nog niet zijn gecached; latere builds zullen sneller zijn. Als je extra command line arguments wilt toevoegen aan het build-commando, dan kun je dat doen aan het einde van de opdrachtregel in plaats van ``. Bijvoorbeeld: als je een geoptimaliseerde releaseversie wilt bouwen, draai dan het bovenstaande commando gevolgd door `--release`. + + Het uitvoerbare bestand, in debug-modus, zal verschijnen in de target-map, en kan als volgt worden uitgevoerd: + +```sh +target/debug/rustdesk +``` + +Als je een release-versie hebt gebouwd, is het commando als volgt: + +```sh +target/release/rustdesk +``` + +Zorg ervoor dat je deze commando's van de root van de RustDesk-repository uitvoert, anders kan het programma de nodige afhankelijkheden mogelijk niet vinden. Let ook op dat andere cargo-subcommando's zoals `install` en `run` zijn momenteel niet ondersteund, aangezien deze zouden worden uitgevoerd in een container in plaats van op de host. + +## Bestandsstructuur + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: videocodec, configuratie, TCP/UDP-wrapper, protobuf, bestandssysteemfuncties voor bestandsoverdracht en nog wat andere nuttige functies +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: schermopname +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platformspecifieke muis- en toetsenbordbeheer +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: geluids-, klembord-, invoer- en video-services, netwerkverbindingen +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: voor het opzetten van peer-verbindingen +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicatie met [rustdesk-server](https://github.com/rustdesk/rustdesk-server), afwachten van redirect op afstand (TCP hole punching) of een relayed verbinding +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platformspecifieke code + +## Snapshot + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-NO.md b/shelled/rustdesk-as-ref/docs/README-NO.md new file mode 100644 index 0000000..1352e8a --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-NO.md @@ -0,0 +1,177 @@ +

+ RustDesk - Your remote desktop
+ Servere • + Build • + Docker • + Struktur • + Snapshot
+ [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk
+ Vi trenger din hjelp til å oversette denne README-en, RustDesk UI og RustDesk Doc tid ditt morsmål +

+ +Snakk med oss: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Avanserte%20Funksjoner-blue)](https://rustdesk.com/pricing.html) + +Enda en annen fjernstyrt desktop programvare, skrevet i Rust. Virker rett ut av pakken, ingen konfigurasjon nødvendig. Du har full kontroll over din data, uten beskymring for sikkerhet. Du kan bruke vår rendezvous_mediator/relay server, [sett opp din egen](https://rustdesk.com/server), eller [skriv din egen rendezvous_mediator/relay server](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](CONTRIBUTING-NO.md) for hjelp med oppstart. + +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**BINARY NEDLASTING**](https://github.com/rustdesk/rustdesk/releases) + +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Få det på F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Få det på Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) + +## Avhengigheter + +Desktop versjoner bruker Flutter eller Sciter (avviklet) for GUI, denne veiledningen er bare for Sciter, grunnet att det er letter og en mer venlig start. Skjekk ut vår [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for bygging av Flutter versjonen. + +Venligst last ned Sciters dynamiske bibliotek selv. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Rå steg for bygging + +- Klargjør ditt Rust development env og C++ build env + +- Installer [vcpkg](https://github.com/microsoft/vcpkg), og koriger `VCPKG_ROOT` env vaiabelen + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/macOS: vcpkg install libvpx libyuv opus aom + +- Kjør `cargo run` + +## [Bygg](https://rustdesk.com/docs/en/dev/build/) + +## Hvordan Bygge til Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Installer vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Fiks libvpx (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Bygg + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Hvordan bygge med Docker + +Start med å klone repositoret og bygg Docker konteineren: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Deretter, hver gang du trenger å bygge applikasjonen, kjør følgene kommando: + +```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 +``` + +Det kan ta lengere tid før avhengighetene blir bufret første gang du bygger, senere bygg er raskere. Hvis du trenger å spesifisere forkjellige argumenter til bygge kommandoen, kan du gjøre det på slutten av kommandoen ved `` feltet. For eksempel, hvis du ville bygge en optimalisert release versjon, ville du kjørt kommandoen over fulgt `--release`. Den kjørbare filen vill være tilgjengelig i mål direktive på ditt system, og kan bli kjørt med: + +```sh +target/debug/rustdesk +``` + +Eller, hvis du kjører ett release program: + +```sh +target/release/rustdesk +``` + +Venligst pass på att du kjører disse kommandoene fra roten av RustDesk repositoret, eller kan det hende att applikasjon ikke finner de riktige ressursene. Pass også på att andre cargo subkommandoer som for eksempel `install` eller `run` ikke støttes med denne metoden da de vill installere eller kjøre programmet i konteineren istedet for verten. + +## Fil Struktur + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodek, configurasjon, tcp/udp innpakning, protobuf, fs funksjon for fil overføring, og noen andre verktøy funksjoner +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: skjermfangst +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform spesefik keyboard/mus kontroll +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: fil kopi og innliming implementasjon for Windows, Linux, macOS. +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: foreldret Sciter UI (avviklet) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: lyd/utklippstavle/input/video tjenester, og internett tilkobling +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start en peer tilkobling +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter nettsted klient + +## Skjermbilder + +![Tilkoblings Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![Koble til Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![Fil Overføring](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) + diff --git a/shelled/rustdesk-as-ref/docs/README-PL.md b/shelled/rustdesk-as-ref/docs/README-PL.md new file mode 100644 index 0000000..437682a --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-PL.md @@ -0,0 +1,169 @@ +

+ RustDesk - Twój zdalny pulpit
+ Serwery • + Kompilacja • + Docker • + Struktura • + Snapshot
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język +

+ +Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Zaawansowane%20Funkcje-blue)](https://rustdesk.com/pricing.html) + +## O projekcie + +RustDesk to wieloplatformowe oprogramowanie do zdalnego pulpitu, napisane w języku Rust, zaprojektowane z myślą o prostocie wdrożenia, bezpieczeństwie i pełnej kontroli użytkownika nad danymi. Aplikacja działa od razu po uruchomieniu i nie wymaga skomplikowanej konfiguracji. Możesz skorzystać z naszego darmowego serwera publicznego, [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING-PL.md`](CONTRIBUTING-PL.md) pomoc w uruchomieniu programu. + +[**PYTANIA I ODPOWIEDZI (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**POBIERANIE BINARIÓW**](https://github.com/rustdesk/rustdesk/releases) + +[**WERSJE TESTOWE (NIGHTLY)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Zależności + +Wersje desktopowe korzystają z biblioteki [sciter](https://sciter.com/) jako silnika GUI. Bibliotekę Sciter należy pobrać i zainstalować samodzielnie. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Podstawowe kroki do kompilacji + +- Przygotuj środowisko programistyczne Rust i środowisko programowania C++ + +- Zainstaluj [vcpkg](https://github.com/microsoft/vcpkg), i ustaw prawidłowo zmienną `VCPKG_ROOT` + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- uruchom `cargo run` + +## Jak Kompilować na Linuxie + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Zainstaluj vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Popraw libvpx (Dla Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Kompilacja + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +cargo run +``` + +## Jak kompilować za pomocą Dockera + +Rozpocznij od sklonowania repozytorium i stworzenia kontenera docker: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Następnie, za każdym razem, gdy potrzebujesz skompilować aplikację, uruchom następujące polecenie: + +```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 +``` + +Zauważ, że pierwsza kompilacja może potrwać dłużej zanim zależności zostaną zbuforowane, kolejne będą szybsze. Dodatkowo, jeśli potrzebujesz określić inne argumenty dla polecenia budowania, możesz to zrobić na końcu komendy w miejscu ``. Na przykład, jeśli chciałbyś zbudować zoptymalizowaną wersję wydania, uruchomiłbyś powyższą komendę a następnie `--release`. Powstały plik wykonywalny będzie dostępny w folderze docelowym w twoim systemie i może być uruchomiony z: + +```sh +target/debug/rustdesk +``` + +Lub jeśli uruchamiasz plik wykonywalny wersji: + +```sh +target/release/rustdesk +``` + +Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium RustDesk, w przeciwnym razie aplikacja może nie być w stanie znaleźć wymaganych zasobów. Należy również pamiętać, że inne podpolecenia ładowania, takie jak `install` lub `run` nie są obecnie obsługiwane za pomocą tej metody, ponieważ instalowałyby lub uruchamiały program wewnątrz kontenera zamiast na hoście. + +## Struktura plików + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: kodek wideo, konfiguracja, obsługa tcp/udp, protobuf, funkcje systemu plików do transferu plików i kilka innych funkcji użytkowych +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: przechwytywanie ekranu +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: specyficzne dla danej platformy sterowanie klawiaturą/myszą +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/schowek/wejście(input)/wideo oraz połączenia sieciowe +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: uruchamia połączenie bezpośrednie +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), czekanie na bezpośrednie (odpytywanie TCP) lub przekazywane połączenie +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: kod specyficzny dla danej platformy +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: kod Flutter dla urządzeń mobilnych +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript dla Flutter - klient web + +## Zrzuty ekranu + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) + diff --git a/shelled/rustdesk-as-ref/docs/README-PTBR.md b/shelled/rustdesk-as-ref/docs/README-PTBR.md new file mode 100644 index 0000000..6c3e6b9 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-PTBR.md @@ -0,0 +1,152 @@ +

+ RustDesk - Seu desktop remoto
+ Servidores • + Compilar • + Docker • + Estrutura • + Screenshots
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Precisamos de sua ajuda para traduzir este README e a UI do RustDesk para sua língua nativa +

+ +Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Recursos%20Avan%C3%A7ados-blue)](https://rustdesk.com/pricing.html) + +Mais um software de desktop remoto, escrito em Rust. Funciona por padrão, sem necessidade de configuração. Você tem completo controle de seus dados, sem se preocupar com segurança. Você pode usar nossos servidores de rendezvous/relay, [configurar seu próprio](https://rustdesk.com/server), ou [escrever seu próprio servidor de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk acolhe contribuições de todos. Leia [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ver como começar. + +[**DOWNLOAD DE BINÃRIOS**](https://github.com/rustdesk/rustdesk/releases) + +## Dependências + +Versões de desktop utilizam [sciter](https://sciter.com/) para a GUI, por favor baixe a biblioteca dinâmica sciter por conta própria. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Compilação crua + +- Prepare seu ambiente de desenvolvimento Rust e ambiente de compilação C++ + +- Instale [vcpkg](https://github.com/microsoft/vcpkg), e configure a variável de ambiente `VCPKG_ROOT` corretamente + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus aom + +- Execute `cargo run` + +## Como compilar no Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Instale vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Conserte libvpx (Para o Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Compile + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Como compilar com Docker + +Comece clonando o repositório e montando o container docker: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Então, sempre que precisar compilar a aplicação, execute este comando: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Note que a primeira compilação pode demorar mais antes que as dependências sejam armazenadas em cache, as compilações subsequentes serão mais rápidas. Adicionalmente, se você precisar especificar argumentos diferentes para o comando de compilação, você pode fazê-lo ao final do comando na posição do ``. Por exemplo, se você gostaria de compilar uma versão de release otimizada, você executaria o comando acima seguido de `--release`. O executável gerado estará disponível no diretório alvo no seu sistema, e pode ser executado com: + +```sh +target/debug/rustdesk +``` + +Ou, se estiver rodando um executável de release: + +```sh +target/release/rustdesk +``` + +Por favor verifique que está executando estes comandos da raiz do repositório do RustDesk, senão a aplicação pode não encontrar os recursos necessários. Note também que outros subcomandos do cargo como `install` ou `run` não são suportados atualmente via este método, já que eles iriam instalar ou rodar o programa dentro do container ao invés do host. + +## Estrutura de arquivos + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec de vídeo, configurações, wrapper de tcp/udp, protobuf, funções de sistema de arquivos para transferência de arquivos, e outras funções utilitárias +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de tela +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controle de teclado/mouse específico a cada plataforma +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: serviços de áudio/área de transferência/entrada/vídeo, e conexões de rede +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar uma conexão "peer to peer" +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicação com [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguardar pela conexão remota direta (TCP hole punching) ou conexão indireta (relayed) +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico a cada plataforma + +> [!Cuidadob] +> **Aviso de uso indevido:**
+> Os desenvolvedores do RustDesk não aprovam nem apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, é estritamente contra nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido da aplicação. + +## Screenshots + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-RO.md b/shelled/rustdesk-as-ref/docs/README-RO.md new file mode 100644 index 0000000..be7ecf1 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-RO.md @@ -0,0 +1,181 @@ +

+ RustDesk - desktopul tău la distanță
+ Construire • + Docker • + Structură • + Capturi
+ [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk] | [Română]
+ Avem nevoie de ajutorul tău pentru a traduce acest README, RustDesk UI și RustDesk Doc în limba ta maternă +

+ +> [!Atenție] +> **Declinare de responsabilitate privind utilizarea abuzivă:**
+> Dezvoltatorii RustDesk nu susțin sau aprobă utilizarea neetică sau ilegală a acestui software. Utilizarea abuzivă, cum ar fi accesul neautorizat, controlul sau invadarea intimității, este strict împotriva regulilor noastre. Autorii nu sunt responsabili pentru utilizarea necorespunzătoare a aplicației. + + +Conversați cu noi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Advanced%20Features-blue)](https://rustdesk.com/pricing.html) + +Încă o soluție de desktop la distanță scrisă în Rust. Funcționează imediat, fără configurare necesară. Ai control total asupra datelor tale, fără probleme de securitate. Poți folosi serverul nostru de rendezvous/relay, [să-ți configurezi propriul server](https://rustdesk.com/server) sau [să scrii propriul server de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). + +![imagine](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk primește contribuții de la oricine. Vezi [CONTRIBUTING.md](../docs/CONTRIBUTING.md) pentru ajutor la început. + +[**ÎNTREBĂRI FRECVENTE (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**DESCĂRCARE BINARE**](https://github.com/rustdesk/rustdesk/releases) + +[**BUILD NIGHTLY**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) + +## Dependențe + +Versiunile desktop folosesc Flutter sau Sciter (depreciat) pentru interfață; acest ghid este pentru Sciter doar, deoarece este mai ușor și mai prietenos pentru început. Vezi [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) pentru construire cu Flutter. + +Te rugăm să descarci singur librăria dinamică Sciter. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Pași pentru construire (Raw Steps to build) + +- Pregătește mediul de dezvoltare Rust și mediul de construire C++ + +- Instalează [vcpkg](https://github.com/microsoft/vcpkg) și setează corect variabila de mediu `VCPKG_ROOT` + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/macOS: vcpkg install libvpx libyuv opus aom + +- rulează `cargo run` + +## [Construire](https://rustdesk.com/docs/en/dev/build/) + +## Cum se construiește pe Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Instalează vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Repară libvpx (Pentru Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Build + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone --recurse-submodules https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Cum să construiești cu Docker + +Începe prin clonarea repository-ului și construirea imaginii Docker: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +git submodule update --init --recursive +docker build -t "rustdesk-builder" . +``` + +Apoi, de fiecare dată când trebuie să construiești aplicația, rulează comanda următoare: + +```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 +``` + +Reține că prima construire poate dura mai mult până când dependențele sunt în cache; construirile ulterioare vor fi mai rapide. De asemenea, dacă trebuie să specifici argumente diferite comenzii de build, le poți adăuga la finalul comenzii în poziția ``. De exemplu, pentru a construi o versiune optimizată de release, adaugă `--release`. Executabilul rezultat va fi disponibil în folderul `target` pe sistemul tău, și poate fi rulat cu: + +```sh +target/debug/rustdesk +``` + +Sau, dacă rulezi un executabil release: + +```sh +target/release/rustdesk +``` + +Asigură-te că rulezi aceste comenzi din rădăcina repository-ului RustDesk, altfel aplicația poate să nu găsească resursele necesare. De asemenea, reține că alte subcomenzi cargo, cum ar fi `install` sau `run`, nu sunt acceptate în prezent prin această metodă, deoarece ar instala sau rula programul în interiorul containerului în loc de gazdă. + +## Structura fișierelor + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funcții fs pentru transfer de fișiere și alte funcții utilitare +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: capturare ecran +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control tastatură/mouse specific platformei +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementare copy/paste pentru fișiere pentru Windows, Linux, macOS. +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: interfață Sciter învechită (depreciată) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servicii audio/clipboard/input/video și conexiuni de rețea +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: inițiază o conexiune peer +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: comunică cu [rustdesk-server](https://github.com/rustdesk/rustdesk-server), așteaptă conexiune directă remote (TCP hole punching) sau prin relay +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: cod specific platformei +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: cod Flutter pentru desktop și mobil +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript pentru clientul Flutter web + +## Capturi de ecran + +![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) diff --git a/shelled/rustdesk-as-ref/docs/README-RU.md b/shelled/rustdesk-as-ref/docs/README-RU.md new file mode 100644 index 0000000..928faad --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-RU.md @@ -0,0 +1,183 @@ +

+ RustDesk - Ваш удаленый рабочий Ñтол
+ Первичные шаги Ð´Ð»Ñ Ñборки • + Как Ñобрать Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Docker • + Структура файлов • + Скриншоты
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ Ðам нужна ваша помощь в переводе Ñтого README, интерфейÑа RustDesk + и документации RustDesk на ваш родной Ñзык. +

+ +> [!Caution] +> **Отказ от ответÑтвенноÑти за неправомерное иÑпользование**
+> Разработчики RustDesk не одобрÑÑŽÑ‚ и не поддерживают какое-либо неÑтичное или незаконное иÑпользование данного программного обеÑпечениÑ. Ðеправомерное иÑпользование (неÑанкционированный доÑтуп, контроль или вторжение в чаÑтную жизнь) Ñтрого противоречит нашим правилам. Ðвторы не неÑут ответÑтвенноÑти за любое неправомерное иÑпользование приложениÑ. + +Общение Ñ Ð½Ð°Ð¼Ð¸: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%D0%A0%D0%B0%D1%81%D1%88%D0%B8%D1%80%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5%20%D0%92%D0%BE%D0%B7%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D0%B8-blue)](https://rustdesk.com/pricing.html) + +Ещё одно программное обеÑпечение Ð´Ð»Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð½Ð¾Ð³Ð¾ рабочего Ñтола, напиÑанное на Rust. Работает из коробки, наÑтройки не требует. Ð’Ñ‹ полноÑтью контролируете Ñвои данные, не беÑпокоÑÑÑŒ о безопаÑноÑти. Ð’Ñ‹ можете иÑпользовать наш Ñервер ретранÑлÑции, [наÑтроить Ñвой ÑобÑтвенный](https://rustdesk.com/server), или [напиÑать Ñвой](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk приветÑтвует вклад каждого. ОзнакомьтеÑÑŒ Ñ [`docs/CONTRIBUTING-RU.md`](CONTRIBUTING-RU.md) в начале работы Ð´Ð»Ñ Ð¿Ð¾Ð½Ð¸Ð¼Ð°Ð½Ð¸Ñ. + +[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) (Ð”Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ Ð½Ð° английÑком Ñзыке) + +[**ЧаÑто задаваемые вопроÑÑ‹**](https://github.com/rustdesk/rustdesk/wiki/FAQ) (Страница на английÑком Ñзыке) + +[**СКÐЧÐТЬ ПРИЛОЖЕÐИЕ**](https://github.com/rustdesk/rustdesk/releases) + +[**ÐОЧÐЫЕ СБОРКИ (Ðктуальные)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) + +## ЗавиÑимоÑти + +Ð”Ð»Ñ ÐŸÐš-верÑии иÑпользуютÑÑ Ð±Ð¸Ð±Ð»Ð¸Ð¾Ñ‚ÐµÐºÐ¸ Flutter или Sciter (уÑтаревшее) Ð´Ð»Ñ Ð³Ñ€Ð°Ñ„Ð¸Ñ‡ÐµÑкого интерфейÑа. Данное руководÑтво подразумевает работу Ñ Sciter, так как он более проÑтой в иÑпользовании и Ñ Ð½Ð¸Ð¼ легче начать работу. Ð’Ñ‹ можете также поÑмотреть на механизм нашего [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) Ð´Ð»Ñ Ñборок на Flutter. + +Загрузите динамичеÑкую библиотеку Flutter ÑамоÑтоÑтельно. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Первичные шаги Ð´Ð»Ñ Ñборки + +- Подготовьте Ñреду разработки Rust и Ñреду Ñборки C++. + +- УÑтановите [vcpkg](https://github.com/microsoft/vcpkg), и правильно уÑтановите переменную `VCPKG_ROOT` + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/macOS: vcpkg install libvpx libyuv opus aom + +- Выполните команду `cargo run` + +## [Сборка](https://rustdesk.com/docs/ru/dev/build/) + +## Как Ñобрать на Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### УÑтановка vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### ИÑправление libvpx (Ð´Ð»Ñ Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Сборка + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone --recurse-submodules https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Как Ñобрать Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Docker + +Ðачните Ñ ÐºÐ»Ð¾Ð½Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ Ð¸ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ docker-контейнера: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +git submodule update --init --recursive +docker build -t "rustdesk-builder" . +``` + +Затем при каждой Ñборке Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½Ñйте Ñледующую команду: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Обратите внимание, что Ð¿ÐµÑ€Ð²Ð°Ñ Ñборка может занÑть больше времени, прежде чем завиÑимоÑти будут кÑшированы, но поÑледующие Ñборки будут выполнÑтьÑÑ Ð±Ñ‹Ñтрее. Кроме того, еÑли вам нужно указать другие аргументы Ð´Ð»Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ñ‹ Ñборки, вы можете Ñделать Ñто в конце команды в переменной ``. Ðапример, еÑли вы хотите Ñоздать оптимизированную верÑию, вы должны выполнить приведенную выше команду и в конце Ñтроки добавить `--release`. Полученный иÑполнÑемый файл будет доÑтупен в целевой папке вашей ÑиÑтемы и может быть запущен Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Ñледующей команды: + +```sh +target/debug/rustdesk +``` + +Или, еÑли вы иÑпользуете иÑполнÑемый файл релиза: + +```sh +target/release/rustdesk +``` + +ПожалуйÑта, убедитеÑÑŒ, что вы запуÑкаете Ñти команды из ÐºÐ¾Ñ€Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ RustDesk, иначе приложение не Ñможет найти необходимые реÑурÑÑ‹. Также обратите внимание, что другие подкоманды Cargo, такие как `install` или `run`, в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½Ðµ поддерживаютÑÑ Ñтим методом, поÑкольку они будут уÑтанавливать или запуÑкать программу внутри контейнера, а не на хоÑте. + +## Структура файлов + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: видеокодек, конфигурациÑ, враппер TCP/UDP, protobuf, функции файловой ÑиÑтемы Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‡Ð¸ файлов и некоторые другие Ñлужебные функции +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: захват Ñкрана +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Ñпецифичное Ð´Ð»Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ñ‹ управление клавиатурой/мышью +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: функционал буфера обмена файлами Ð´Ð»Ñ Windows, Linux, и macOS +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графичеÑкий пользовательÑкий Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð½Ð° Sciter (уÑтаревшее) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ÑервиÑÑ‹ аудио, буфера обмена, ввода, видео и Ñетевых подключений +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: одноранговое Ñоединение +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: ÑвÑзь Ñ [Ñервером RustDesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прÑмого (через TCP hole punching) или ретранÑлируемого ÑÐ¾ÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Ñпецифичный Ð´Ð»Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ñ‹ код +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter Ð´Ð»Ñ ÐŸÐš-верÑии и мобильных уÑтройÑтв +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript Ð´Ð»Ñ Web-клиента Flutter + +## Скриншоты + +![Менеджер Ñоединений](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![Подключение к удалённому рабочему Ñтолу на Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![Передача файлов](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP-туннелирование](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/docs/README-TR.md b/shelled/rustdesk-as-ref/docs/README-TR.md new file mode 100644 index 0000000..99c961e --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-TR.md @@ -0,0 +1,181 @@ + +

+ RustDesk - Uzak masaüstü uygulamanız
+ Sunucular • + Derleme • + Docker ile Derleme • + Dosya Yapısı • + Ekran Görüntüleri
+ [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά]
+ README, RustDesk UI ve RustDesk Dökümantasyonu'nu ana dilinize çevirmemiz için yardımınıza ihtiyacımız var +

+ + +> [!Dikkat] +> **Yanlış Kullanım Uyarısı:**
+> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir. + +Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Geli%C5%9Fmi%C5%9F%20%C3%96zellikler-blue)](https://rustdesk.com/pricing.html) + +Rust dilinde yazılmış, başka bir uzak masaüstü yazılımı daha. Hiçbir yapılandırma gerekmeksizin, hemen kullanıma hazır. Güvenlik konusunda hiçbir endişe duymadan, verileriniz üzerinde tam kontrole sahip olun. Kendi rendezvous/relay sunucumuzu kullanabilirsiniz, [kendi sunucunuzu kurabilirsiniz](https://rustdesk.com/server) veya [kendi rendezvous/relay sunucunuzu yazabilirsiniz](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk, herkesin katkısına açıktır. Başlamak için [CONTRIBUTING.md](CONTRIBUTING-TR.md) belgesine göz atın. + +[**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**BINARY İNDİR**](https://github.com/rustdesk/rustdesk/releases) + +[**NIGHTLY DERLEME**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[F-Droid'de Alın](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Gereksinimler + +Masaüstü sürümleri GUI için; [Sciter](https://sciter.com/)(kaldırılacak) veya Flutter kullanır. Sciter daha kolay ve başlamak için daha dostcanlısı, bundan dolayı bu kılavuz sadece Sciter içindir. Flutter sürümünü derlemek için [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)'ımıza bakın. + +Lütfen Sciter dinamik kütüphanesini kendiniz indirin. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Temel Derleme Adımları + +- Rust geliştirme ortamınızı ve C++ derleme ortamınızı hazırlayın. + +- [vcpkg](https://github.com/microsoft/vcpkg) yükleyin ve `VCPKG_ROOT` ortam değişkenini doğru bir şekilde ayarlayın. + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/macOS: vcpkg install libvpx libyuv opus aom + +- `cargo run` komutunu çalıştırın. + +## [Derleme](https://rustdesk.com/docs/en/dev/build/) + +## Linux Üzerinde Derleme Nasıl Yapılır + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### vcpkg'yi Yükleyin + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### libvpx'i Düzeltin (Fedora için) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Derleme + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Docker ile Derleme Nasıl Yapılır + +Önce repository'i klonlayın ve Docker container'ını oluşturun. + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Ardından, uygulamayı her derlemeniz gerektiğinde aşağıdaki komutu çalıştırın: + +```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 +``` + +Bilin ki ilk derlemeniz gereksinimlerin önbelleği yüklenmesinden ötürü uzun sürebilir, sonraki derlemeleriniz daha hızlı olacaktır. Ayrıca, derleme komutuna isteğe bağlı argümanlar belirtmeniz gerekiyorsa, bunu komutun sonunda ki `` yerine yazabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan çalıştırılabilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir olacaktır: + +```sh +target/debug/rustdesk +``` + +Veya, yayım çalıştırılabilir dosyası için: + +```sh +target/release/rustdesk +``` + +Lütfen bu komutları RustDesk reposunun root klasöründe çalıştırdığınızdan emin olun, aksi takdirde uygulama gereken kaynakları bulamayabilir. Ayrıca, `install` veya `run` gibi diğer cargo altkomutları şu anda bu yöntem aracılığıyla desteklenmemektedir, çünkü bunlar programı konteyner içinde kurar veya çalıştırır, ana makinede değil. + +## Dosya Yapısı + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, dosya transferi için fs fonksiyonları ve diğer bazı yardımcı işlevler +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ekran yakalama +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platforma özgü klavye/fare kontrolü +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: platforma özgü kopyala/yapıştır implementasyonları. +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: Eski Sciter UI (kaldırılacak) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pano/input/video servisleri ve ağ bağlantıları +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Eşli bağlantı başlat +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişime gir, remote direct(TCP delik açma) yada relay bağlantısı için bekle +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Masaüstü ve mobil için Flutter kodu +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter web istemcisi için JavaScript + + +## Ekran Görüntüleri + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) +``` diff --git a/shelled/rustdesk-as-ref/docs/README-UA.md b/shelled/rustdesk-as-ref/docs/README-UA.md new file mode 100644 index 0000000..eb4c9ed --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-UA.md @@ -0,0 +1,174 @@ +

+ RustDesk - Ваша віддалена ÑтільницÑ
+ Сервери • + Ð—Ð±Ð¸Ñ€Ð°Ð½Ð½Ñ â€¢ + Docker • + Структура • + Знімки екрана
+ [English] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
+ Ðам потрібна ваша допомога Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐºÐ»Ð°Ð´Ñƒ цього README, інтерфейÑу та документації RustDesk вашою рідною мовою +

+ +Ð¡Ð¿Ñ–Ð»ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð· нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%D0%A0%D0%BE%D0%B7%D1%88%D0%B8%D1%80%D0%B5%D0%BD%D1%96%20%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D1%96%D1%97-blue)](https://rustdesk.com/pricing.html) + +Ще один заÑтоÑунок Ð´Ð»Ñ Ð²Ñ–Ð´Ð´Ð°Ð»ÐµÐ½Ð¾Ð³Ð¾ ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñтільницею, напиÑаний на Rust. Працює з коробки, не потребує налаштуваннÑ. Ви повніÑтю контролюєте Ñвої дані, не турбуючиÑÑŒ про безпеку. Ви можете викориÑтовувати наш Ñервер ретранÑлÑції, [налаштувати Ñвій влаÑний](https://rustdesk.com/server), або [напиÑати Ñвій влаÑний Ñервер ретранÑлÑції](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk вітає внеÑок кожного. ОзнайомтеÑÑ Ð· [CONTRIBUTING.md](CONTRIBUTING.md), щоб отримати допомогу на початковому етапі. + +[**ЧаПи**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**ЗÐÐ’ÐÐТÐЖЕÐÐЯ ЗÐСТОСУÐКУ**](https://github.com/rustdesk/rustdesk/releases) + +[**ÐІЧÐІ ЗБІРКИ**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## ЗалежноÑті + +Стільничні верÑÑ–Ñ— викориÑтовують Flutter чи Sciter (заÑтаріле) Ð´Ð»Ñ Ð³Ñ€Ð°Ñ„Ñ–Ñ‡Ð½Ð¾Ð³Ð¾ інтерфейÑу. Ð¦Ñ Ñ–Ð½ÑÑ‚Ñ€ÑƒÐºÑ†Ñ–Ñ Ð»Ð¸ÑˆÐµ Ð´Ð»Ñ Sciter, оÑкільки він Ñ” більш проÑтим та дружнім Ð´Ð»Ñ Ð¿Ð¾Ñ‡Ð°Ñ‚ÐºÑ–Ð²Ñ†Ñ–Ð². ПереглÑньте [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) Ð´Ð»Ñ Ð·Ð±Ñ–Ñ€ÐºÐ¸ верÑÑ–Ñ— на Flutter. + +Будь лаÑка, завантажте динамічну бібліотеку Sciter ÑамоÑтійно. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Кроки Ð´Ð»Ñ Ð·Ð±Ñ–Ñ€ÐºÐ¸ + +- Підготуйте Ñередовище розробки Rust Ñ– Ñередовище Ð·Ð±Ð¸Ñ€Ð°Ð½Ð½Ñ C++. + +- Ð’Ñтановіть [vcpkg](https://github.com/microsoft/vcpkg), Ñ– правильно вÑтановіть змінну `VCPKG_ROOT`. + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/macOS: vcpkg install libvpx libyuv opus aom + +- ЗапуÑтіть `cargo run` + +## [ЗбираннÑ](https://rustdesk.com/docs/en/dev/build/) + +## Як зібрати на Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Ð’ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Ð’Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ libvpx (Ð´Ð»Ñ Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Ð—Ð±Ð¸Ñ€Ð°Ð½Ð½Ñ + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Як зібрати за допомогою Docker + +Почніть з ÐºÐ»Ð¾Ð½ÑƒÐ²Ð°Ð½Ð½Ñ Ñховища та ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ docker-контейнера: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Ðадалі щоразу, коли вам буде потрібно зібрати заÑтоÑунок, запуÑкайте таку команду: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +Зверніть увагу, що перша збірка може зайнÑти більше чаÑу, перш ніж залежноÑті будуть кешовані, але наÑтупні збірки будуть виконуватиÑÑ ÑˆÐ²Ð¸Ð´ÑˆÐµ. Крім того, Ñкщо вам потрібно вказати інші аргументи Ð´Ð»Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¸ збірки, ви можете зробити це в кінці команди у змінній ``. Ðаприклад, Ñкщо ви хочете Ñтворити оптимізовану верÑÑ–ÑŽ, ви маєте запуÑтити наведену вище команду Ñ– в кінці Ñ€Ñдка додати `--release`. Отриманий виконуваний файл буде доÑтупний у цільовій папці вашої ÑиÑтеми Ñ– може бути запущений за допомогою: + +```sh +target/debug/rustdesk +``` + +Ðбо, Ñкщо ви викориÑтовуєте виконуваний файл релізу: + +```sh +target/release/rustdesk +``` + +Будь лаÑка, переконайтеÑÑ, що ви запуÑкаєте ці команди з ÐºÐ¾Ñ€ÐµÐ½Ñ Ñховища RustDesk, інакше додаток не зможе знайти необхідні реÑурÑи. Також зверніть увагу, що інші cargo підкоманди, такі Ñк `install` або `run`, наразі не підтримуютьÑÑ Ñ†Ð¸Ð¼ методом, оÑкільки вони будуть вÑтановлювати або запуÑкати програму вÑередині контейнера, а не на хоÑті. + +## Структура файлів + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: відеокодек, конфіг, обгортка tcp/udp, protobuf, функції fs Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ð²Ð°Ð½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñ–Ð² Ñ– деÑкі інші Ñлужбові функції +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Ð·Ð°Ñ…Ð¾Ð¿Ð»ÐµÐ½Ð½Ñ ÐµÐºÑ€Ð°Ð½Ð° +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Ñпецифічне Ð´Ð»Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ð¸ ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ»Ð°Ð²Ñ–Ð°Ñ‚ÑƒÑ€Ð¾ÑŽ/мишею +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Ñ€ÐµÐ°Ð»Ñ–Ð·Ð°Ñ†Ñ–Ñ ÐºÐ¾Ð¿Ñ–ÑŽÐ²Ð°Ð½Ð½Ñ Ñ‚Ð° вÑÑ‚Ð°Ð²Ð»ÐµÐ½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñ–Ð² Ð´Ð»Ñ Windows, Linux, macOS. +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний Ñ–Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ ÐºÐ¾Ñ€Ð¸Ñтувача +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ÑервіÑи аудіо/буфера обміну/вводу/відео та мережевих підключень +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове Ð·Ê¼Ñ”Ð´Ð½Ð°Ð½Ð½Ñ +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: ÐºÐ¾Ð¼ÑƒÐ½Ñ–ÐºÐ°Ñ†Ñ–Ñ Ð· [rustdesk-server](https://github.com/rustdesk/rustdesk-server), Ð¾Ñ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ñ–Ð´Ð´Ð°Ð»ÐµÐ½Ð¾Ð³Ð¾ прÑмого (обхід TCP NAT) або ретранÑльованого Ð·Ê¼Ñ”Ð´Ð½Ð°Ð½Ð½Ñ +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Ñпецифічний Ð´Ð»Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ð¸ код +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter Ð´Ð»Ñ Ð¼Ð¾Ð±Ñ–Ð»ÑŒÐ½Ð¸Ñ… приÑтроїв +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript Ð´Ð»Ñ Ð²ÐµÐ± клієнта на Flutter + +## Знімки екрана + +![Менеджер зʼєднань](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![ÐŸÑ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð´Ð¾ ПК з Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![Передача файлів](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![Ð¢ÑƒÐ½ÐµÐ»ÑŽÐ²Ð°Ð½Ð½Ñ TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) + diff --git a/shelled/rustdesk-as-ref/docs/README-VN.md b/shelled/rustdesk-as-ref/docs/README-VN.md new file mode 100644 index 0000000..38cdc10 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-VN.md @@ -0,0 +1,161 @@ + + +

+ RustDesk - Your remote desktop
+ Server • + Build • + Docker • + Structure • + Snapshot
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Ελληνικά]
+ Chúng tôi rất hoan nghênh sá»± há»— trợ cá»§a bạn trong việc dịch trang README, trang giao diện ngưá»i dùng cá»§a RustDesk - RustDesk UI và trang tài liệu cá»§a RustDesk - RustDesk Doc sang Tiếng Việt +

+ +Hãy trao đổi vá»›i chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-T%C3%ADnh%20N%C4%83ng%20N%C3%A2ng%20Cao-blue)](https://rustdesk.com/pricing.html) + +RustDesk là má»™t phần má»m Ä‘iểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyá»n kiểm soát vá»›i dữ liệu cá»§a mình mà không cần phải lo lắng vá» vấn đỠbảo mật. Bạn có thể sá»­ dụng máy chá»§ rendezvous/relay cá»§a chúng tôi hoặc [tá»± cài đặt máy chá»§ cá»§a riêng mình](https://rustdesk.com/server) hay thậm chí [tá»± tạo máy chá»§ rendezvous/relay cho riêng bạn](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +**RustDesk** luôn hoan nghênh má»i đóng góp từ má»i ngưá»i. Hãy xem tệp [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) để bắt đầu. + +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/FAQreleases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Dependencies + +Phiên bản máy tính sá»­ dụng __Flutter__ hoặc __Sciter__ (đã lá»—i thá»i) cho giao diện ngưá»i dùng (GUI). Hướng dẫn này chỉ áp dụng cho phiên bản Sciter, vì nó thân thiện và dá»… bắt đầu hÆ¡n. Hãy kiểm tra [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) cá»§a chúng tôi để xây dá»±ng phiên bản Flutter. + +Vui lòng tá»± tải thư viện `Sciter` vá» máy theo hướng dẫn cho từng hệ Ä‘iá»u hành. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Các bước build cÆ¡ bản + +- Chuẩn bị môi trưá»ng phát triển Rust và môi trưá»ng biên dịch C++ + +- Tải và cài đặt [`vcpkg`](https://github.com/microsoft/vcpkg), và thiết lập biến môi trưá»ng `VCPKG_ROOT`. + + - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static` + - Linux/MacOS: `vcpkg install libvpx libyuv opus aom` +- Chạy lệnh `cargo run` + +## [Build](https://rustdesk.com/docs/en/dev/build/) + +## Cách build cho Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Cách cài đặt `vcpkg` + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Cách sá»­a lá»—i `libvpx` (Dành cho hệ Ä‘iá»u hành Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Build + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Cách build bằng Docker + +Bắt đầu bằng cách sao chép repo này vá» máy tính cá»§a bạn và tạo Docker container: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Sau đó, má»—i khi bạn chạy ứng dụng, thì hãy chạy dòng lệnh sau: + +```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 +``` + +Lưu ý rằng **lần build đầu tiên có thể mất thá»i gian hÆ¡n trước khi các dependencies được lưu vào bá»™ nhá»› cache**, nhưng các lần build sau sẽ nhanh hÆ¡n. Ngoài ra, nếu bạn cần chỉ định các đối số khác cho lệnh build, bạn có thể thêm chúng vào cuối lệnh ở phần ``. Ví dụ, nếu bạn muốn build phiên bản tối ưu hóa, bạn sẽ chạy lệnh trên vá»›i tùy chá»n `--release`. Kết quả biên dịch sẽ được lưu trong thư mục target trên máy tính cá»§a bạn, và có thể chạy vá»›i lệnh: + +```sh +target/debug/rustdesk +``` + +Nếu bạn Ä‘ang chạy bản build được tối ưu hóa, thì bạn có thể chạy vá»›i lệnh: + +```sh +target/release/rustdesk +``` + +Hãy đảm bảo rằng bạn Ä‘ang chạy các lệnh này từ gốc cá»§a thư mục **RustDesk**, nếu không, ứng dụng có thể không thể tìm thấy các tệp tài nguyên cần thiết. Hãy lưu ý rằng các câu lệnh con khác cá»§a **cargo** như **install** hoặc **run** hiện không được há»— trợ qua phương pháp này, vì chúng sẽ cài đặt hoặc chạy chương trình bên trong **container** thay vì trên máy tính cá»§a bạn. + +## Cấu trúc tệp tin + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyá»n file, và má»™t số hàm tiện ích khác +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ghi lại màn hình +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Ä‘iá»u khiển máy tính/chuá»™t trên các ná»n tảng khác nhau +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện ngưá»i dùng +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bắt đầu kết nối vá»›i má»™t peer +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp vá»›i [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trá»±c tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp. +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho má»—i ná»n tảng +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và Ä‘iện thoại +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter + +## Snapshot + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/README-ZH.md b/shelled/rustdesk-as-ref/docs/README-ZH.md new file mode 100644 index 0000000..9328e52 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/README-ZH.md @@ -0,0 +1,233 @@ +

+ RustDesk - Your remote desktop
+ æœåС噍 • + 编译 • + Docker • + 结构 • + 截图
+ [English] | [УкраїнÑька] | [Äesky] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+

+ +> [!CAUTION] +> **å…责声明:**
+> RustDesk 的开å‘人员ä¸çºµå®¹æˆ–支æŒä»»ä½•ä¸é“å¾·æˆ–éžæ³•çš„è½¯ä»¶ä½¿ç”¨è¡Œä¸ºã€‚æ»¥ç”¨è¡Œä¸ºï¼Œä¾‹å¦‚æœªç»æŽˆæƒçš„è®¿é—®ã€æŽ§åˆ¶æˆ–ä¾µçŠ¯éšç§ï¼Œä¸¥æ ¼è¿å我们的准则。作者对应用程åºçš„任何滥用行为概ä¸è´Ÿè´£ã€‚ + +与我们交æµ: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-%E9%AB%98%E7%BA%A7%E5%8A%9F%E8%83%BD-blue)](https://rustdesk.com/pricing.html) + +远程桌é¢è½¯ä»¶ï¼Œå¼€ç®±å³ç”¨ï¼Œæ— éœ€ä»»ä½•é…置。您完全掌控数æ®ï¼Œä¸ç”¨æ‹…心安全问题。您å¯ä»¥ä½¿ç”¨æˆ‘们的注册/中继æœåŠ¡å™¨ï¼Œ +或者[自己设置](https://rustdesk.com/server), +亦或者[开呿‚¨çš„版本](https://github.com/rustdesk/rustdesk-server-demo)。 + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk 期待å„ä½çš„贡献. 如何å‚与开å‘? 详情请看 [CONTRIBUTING-ZH.md](CONTRIBUTING-ZH.md). + +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## ä¾èµ– + +桌é¢ç‰ˆæœ¬ä½¿ç”¨ Flutter 或 Sciter(已弃用)作为 GUI,本教程仅适用于 Sciter,因为它更简å•且更易于上手。查看我们的[CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)以构建 Flutter 版本。 + +请自行下载Sciter动æ€åº“。 + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## 基本构建步骤 + +- 请准备好 Rust å¼€å‘环境和 C++ 编译环境 + +- 安装 [vcpkg](https://github.com/microsoft/vcpkg), 正确设置 `VCPKG_ROOT` 环境å˜é‡ + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/macOS: vcpkg install libvpx libyuv opus aom + +- è¿è¡Œ `cargo run` + +## [构建](https://rustdesk.com/docs/en/dev/build/) + +## 在 Linux 上编译 + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### 安装 vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### ä¿®å¤ libvpx (仅仅针对 Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### 构建 + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## 使用 Docker 编译 + +克隆版本库并构建 Docker 容器: + +```sh +git clone https://github.com/rustdesk/rustdesk # 克隆Github存储库 +cd rustdesk # 进入文件夹 +docker build -t "rustdesk-builder" . # 构建容器 +``` + +请注æ„: +* 针对国内网络访问问题,å¯ä»¥åšä»¥ä¸‹å‡ ç‚¹ä¼˜åŒ–: + 1. Dockerfile 中修改系统的æºåˆ°å›½å†…é•œåƒ + ``` + 在Dockerfileçš„RUN apt update之剿’入两行: + + RUN sed -i "s|deb.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list && \ + sed -i "s|security.debian.org|mirrors.aliyun.com|g" /etc/apt/sources.list + ``` + + 2. 修改容器系统中的 cargo æºï¼Œåœ¨`RUN ./rustup.sh -y`åŽæ’入下é¢ä»£ç ï¼š + + ``` + RUN echo '[source.crates-io]' > ~/.cargo/config \ + && echo 'registry = "https://github.com/rust-lang/crates.io-index"' >> ~/.cargo/config \ + && echo '# æ›¿æ¢æˆä½ åå¥½çš„é•œåƒæº' >> ~/.cargo/config \ + && echo "replace-with = 'sjtu'" >> ~/.cargo/config \ + && echo '# 上海交通大学' >> ~/.cargo/config \ + && echo '[source.sjtu]' >> ~/.cargo/config \ + && echo 'registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"' >> ~/.cargo/config \ + && echo '' >> ~/.cargo/config + ``` + + 3. Dockerfile 中加入代ç†çš„ env + + ``` + 在User rootåŽæ’入两行 + + ENV http_proxy=http://host:port + ENV https_proxy=http://host:port + ``` + + 4. docker build 命令åŽé¢åŠ ä¸Š proxy 傿•° + + ``` + docker build -t "rustdesk-builder" . --build-arg http_proxy=http://host:port --build-arg https_proxy=http://host:port + ``` + +### 构建 RustDesk ç¨‹åº + +ç„¶åŽ, æ¯æ¬¡éœ€è¦æž„å»ºåº”ç”¨ç¨‹åºæ—¶, è¿è¡Œä»¥ä¸‹å‘½ä»¤: + +```sh +docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder +``` + +请注æ„: +* 因为需è¦ç¼“å­˜ä¾èµ–项,首次构建一般很慢(国内网络会ç»å¸¸å‡ºçŽ°æ‹‰å–失败,å¯ä»¥å¤šè¯•几次)。 +* å¦‚æžœæ‚¨éœ€è¦æ·»åŠ ä¸åŒçš„æž„å»ºå‚æ•°ï¼Œå¯ä»¥åœ¨æŒ‡ä»¤æœ«å°¾çš„`` ä½ç½®è¿›è¡Œä¿®æ”¹ã€‚例如构建一个"Release"版本,在指令åŽé¢åŠ ä¸Š` --release`å³å¯ã€‚ +* 如果出现以下的æç¤ºï¼Œåˆ™æ˜¯æ— æƒé™é—®é¢˜ï¼Œå¯ä»¥å°è¯•把`-e PUID="$(id -u)" -e PGID="$(id -g)"`傿•°åŽ»æŽ‰ã€‚ + ``` + usermod: user user is currently used by process 1 + groupmod: Permission denied. + groupmod: cannot lock /etc/group; try again later. + ``` + > **原因:** 容器的 entrypoint 脚本会检测 UID å’Œ GID,在度判和给定的环境å˜é‡çš„ä¸ä¸€è‡´æ—¶ï¼Œä¼šå¼ºè¡Œä¿®æ”¹ user çš„ UID å’Œ GID 并釿–°è¿è¡Œã€‚但在é‡å¯åŽè¯»ä¸åˆ°çŽ¯å¢ƒä¸­çš„ UID å’Œ GID,然åŽå†æ¬¡è¿›å…¥åˆ¤é”™é‡å¯çŽ¯èŠ‚ + + +### è¿è¡Œ RustDesk ç¨‹åº + +生æˆçš„坿‰§è¡Œç¨‹åºåœ¨ target 目录下,å¯ç›´æŽ¥é€šè¿‡æŒ‡ä»¤è¿è¡Œè°ƒè¯• (Debug) 版本的 RustDesk: +```sh +target/debug/rustdesk +``` + +或者您想è¿è¡Œå‘行 (Release) 版本: + +```sh +target/release/rustdesk +``` + +请注æ„: +* 请ä¿è¯æ‚¨è¿è¡Œçš„目录是在 RustDesk 库的根目录内,å¦åˆ™è½¯ä»¶ä¼šè¯»ä¸åˆ°æ–‡ä»¶ã€‚ +* `install`ã€`run`ç­‰ Cargo çš„å­æŒ‡ä»¤åœ¨å®¹å™¨å†…ä¸å¯ç”¨ï¼Œå®¿ä¸»æœºæ‰è¡Œã€‚ + +## 文件结构 + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解ç , é…ç½®, tcp/udp å°è£…, protobuf, 文件传输相关文件系统æ“作函数, 以åŠä¸€äº›å…¶ä»–实用函数 +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: å±å¹•æˆªå– +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: å¹³å°ç›¸å…³çš„鼠标键盘输入 +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windowsã€Linuxã€macOS 的文件å¤åˆ¶å’Œç²˜è´´å®žçް +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: 过时的 Sciter UI(已弃用) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端æœåŠ¡éŸ³é¢‘ã€å‰ªåˆ‡æ¿ã€è¾“å…¥ã€è§†é¢‘æœåŠ¡ã€ç½‘络连接的实现 +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端 +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)ä¿æŒUDP通讯, 等待远程连接(通过打洞直连或者中继) +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平尿œåŠ¡ç›¸å…³ä»£ç  +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌é¢å’Œç§»åŠ¨è®¾å¤‡çš„ Flutter ä»£ç  +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascriptä»£ç  + +## 截图 + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/shelled/rustdesk-as-ref/docs/SECURITY-DE.md b/shelled/rustdesk-as-ref/docs/SECURITY-DE.md new file mode 100644 index 0000000..65f4f79 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/SECURITY-DE.md @@ -0,0 +1,9 @@ +# Sicherheitsrichtlinie + +## Melden einer Schwachstelle + +Wir legen großen Wert auf die Sicherheit des Projekts. Wir ermutigen alle Benutzer, uns alle Sicherheitslücken zu melden, die sie entdecken. +Wenn Sie eine Sicherheitslücke im RustDesk-Projekt finden, melden Sie diese bitte verantwortungsbewusst per E-Mail an info@rustdesk.com. + +Zum jetzigen Zeitpunkt haben wir kein Bug-Bounty-Programm. Wir sind ein kleines Team, das versucht, ein großes Problem zu lösen. Wir bitten Sie dringend, +alle Schwachstellen verantwortungsbewusst zu melden, damit wir weiterhin eine sichere Anwendung für die ganze Gemeinschaft entwickeln können. diff --git a/shelled/rustdesk-as-ref/docs/SECURITY-IT.md b/shelled/rustdesk-as-ref/docs/SECURITY-IT.md new file mode 100644 index 0000000..91573dc --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/SECURITY-IT.md @@ -0,0 +1,11 @@ +# Policy sicurezza + +## Segnalazione di una vulnerabilità + +Attribuiamo grande importanza alla sicurezza del progetto. +Incoraggiamo tutti gli utenti a segnalare eventuali vulnerabilità di sicurezza che ci scoprono. +Se trovi una vulnerabilità nel progetto RustDesk, segnalala responsabilmente inviando un'email a info@rustdesk.com. + +Al momento non abbiamo un programma di taglia sui bug. +Siamo una piccola squadra che cerca di risolvere un grosso problema. +Ti esortiamo a segnalare responsabilmente tutte le vulnerabilità in modo da poter continuare a sviluppare un'applicazione sicura per l'intera comunità. diff --git a/shelled/rustdesk-as-ref/docs/SECURITY-JP.md b/shelled/rustdesk-as-ref/docs/SECURITY-JP.md new file mode 100644 index 0000000..bddf6d9 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/SECURITY-JP.md @@ -0,0 +1,9 @@ +# セキュリティãƒãƒªã‚·ãƒ¼ + +## 脆弱性ã®å ±å‘Š + +ç§ãŸã¡ã¯ãƒ—ロジェクトã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ã‚’éžå¸¸ã«é‡è¦–ã—ã¦ã„ã¾ã™ã€‚ç§ãŸã¡ã¯ã€ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒè„†å¼±æ€§ã‚’発見ã—ãŸå ´åˆã€ç§ãŸã¡ã«å ±å‘Šã™ã‚‹ã“ã¨ã‚’奨励ã—ã¦ã„ã¾ã™ã€‚ +RustDesk プロジェクトã«ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ã®è„†å¼±æ€§ã‚’発見ã—ãŸå ´åˆã¯ã€info@rustdesk.com ã¾ã§ãƒ¡ãƒ¼ãƒ«ã§è²¬ä»»ã‚’æŒã£ã¦å ±å‘Šã—ã¦ãã ã•ã„。 + +ç¾æ™‚点ã§ã¯ã€ãƒã‚°å ±å¥¨é‡‘制度ã¯ã‚りã¾ã›ã‚“。ç§ãŸã¡ã¯å¤§ããªå•題を解決ã—よã†ã¨ã—ã¦ã„ã‚‹å°ã•ãªãƒãƒ¼ãƒ ã§ã™ã€‚コミュニティ全体ã®ãŸã‚ã«å®‰å…¨ãªã‚¢ãƒ—リケーションを作り続ã‘ã‚‹ã“ã¨ãŒã§ãるよã†ã€ +責任をæŒã£ã¦è„†å¼±æ€§ã‚’報告ã—ã¦ãã ã•ã„。 diff --git a/shelled/rustdesk-as-ref/docs/SECURITY-KR.md b/shelled/rustdesk-as-ref/docs/SECURITY-KR.md new file mode 100644 index 0000000..94ce8f2 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/SECURITY-KR.md @@ -0,0 +1,7 @@ +# 보안 ì •ì±… + +## ì·¨ì•½ì  ë³´ê³  + +ì €í¬ëŠ” 프로ì íŠ¸ì˜ ë³´ì•ˆì„ ë§¤ìš° 중요하게 ìƒê°í•©ë‹ˆë‹¤. 모든 사용ìžê°€ 발견한 취약ì ì„ ì €í¬ì—게 ë³´ê³ í•  ê²ƒì„ ê¶Œìž¥í•©ë‹ˆë‹¤. RustDesk 프로ì íЏì—서 보안 취약ì ì´ 발견ë˜ë©´ info@rustdesk.com으로 ì´ë©”ì¼ì„ ë³´ë‚´ ì±…ìž„ê° ìžˆê²Œ ë³´ê³ í•´ 주시기 ë°”ëžë‹ˆë‹¤. + +현재로서는 버그 현ìƒê¸ˆ í”„ë¡œê·¸ëž¨ì´ ì—†ìŠµë‹ˆë‹¤. ì €í¬ëŠ” í° ë¬¸ì œë¥¼ 해결하기 위해 노력하는 소규모 팀입니다. ì „ì²´ 커뮤니티를 위한 안전한 ì‘ìš© í”„ë¡œê·¸ëž¨ì„ ê³„ì† êµ¬ì¶•í•  수 있ë„ë¡ ì·¨ì•½ì ì„ ì±…ìž„ê° ìžˆê²Œ ì‹ ê³ í•´ 주시기 ë°”ëžë‹ˆë‹¤. diff --git a/shelled/rustdesk-as-ref/docs/SECURITY-NL.md b/shelled/rustdesk-as-ref/docs/SECURITY-NL.md new file mode 100644 index 0000000..463b322 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/SECURITY-NL.md @@ -0,0 +1,11 @@ +# Veiligheidsbeleid + +## Een Kwetsbaarheid Melden + +Wij hechten veel waarde aan de veiligheid van het project. We moedigen alle gebruikers aan om kwetsbaarheden die ze ontdekken +aan ons te melden. Als u een beveiligingslek in het RustDesk project vindt, meld dit dan op verantwoorde wijze door +een e-mail te sturen naar info@rustdesk.com. + +Op dit moment hebben we geen bug premie programma. We zijn een klein team dat een groot probleem probeert op te lossen. +We verzoeken u dringend om alle kwetsbaarheden op verantwoorde wijze te melden, zodat we verder kunnen bouwen aan +een veilige applicatie voor de hele gemeenschap. diff --git a/shelled/rustdesk-as-ref/docs/SECURITY-NO.md b/shelled/rustdesk-as-ref/docs/SECURITY-NO.md new file mode 100644 index 0000000..1f8dcb4 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/SECURITY-NO.md @@ -0,0 +1,9 @@ +# Sikkerhets Rettningslinjer + +## Reportering av en SÃ¥rbarhet + +Vi verdsetter pris pÃ¥ sikkerhet for prosjektet høyt. Og oppmunterer alle brukere til Ã¥ rapportere sÃ¥rbarheter de oppdager til oss. +Om du finner en sikkerhets sÃ¥rbarhet i RustDesk prosjektet, venligst raportere det ansvarsfult ved Ã¥ sende oss en email til info@rustdesk.com. + +PÃ¥ dette tidspunktet har vi ingen bug dusør program. Vi er ett lite team som prøver Ã¥ løse ett stort problem. Vi trenger att du raporterer alle sÃ¥rbarhetene +annsvarfult sÃ¥ vi kan fortsettte Ã¥ bygge ett en sikker applikasjon for hele felleskapet. diff --git a/shelled/rustdesk-as-ref/docs/SECURITY-PL.md b/shelled/rustdesk-as-ref/docs/SECURITY-PL.md new file mode 100644 index 0000000..0d4975b --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/SECURITY-PL.md @@ -0,0 +1,9 @@ +# Polityka bezpieczeÅ„stwa + +## ZgÅ‚aszanie podatnoÅ›ci + +Bardzo cenimy sobie bezpieczeÅ„stwo projektu. ZachÄ™camy wszystkich użytkowników do zgÅ‚aszania nam wszelkich wykrytych luk. +Jeżeli znajdziesz lukÄ™ w projekcie RustDesk, proszÄ™ zgÅ‚osić jÄ… jak najszybciej wysyÅ‚ajÄ…c e-mail na adres info@rustdesk.com. + +W tym momencie, nie mamy uruchomionego programu nagradzania za wykryte błędy. JesteÅ›my maÅ‚ym zespoÅ‚em próbujÄ…cym rozwiÄ…zywać duże problemy. +Prosimy o odpowidzialne zgÅ‚aszanie wszelkich podatnoÅ›ci w zabezpieczeniach, abyÅ›my mogli kontynuować tworzenie bezpiecznej aplikacji dla caÅ‚ej spoÅ‚ecznoÅ›ci. diff --git a/shelled/rustdesk-as-ref/docs/SECURITY-RO.md b/shelled/rustdesk-as-ref/docs/SECURITY-RO.md new file mode 100644 index 0000000..029e01d --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/SECURITY-RO.md @@ -0,0 +1,9 @@ +# Politica de Securitate + +## Raportarea unei Vulnerabilități + +Acordăm o mare importanță securității proiectului. ÃŽncurajăm toÈ›i utilizatorii să ne raporteze orice vulnerabilități pe care le descoperă. +Dacă găseÈ™ti o vulnerabilitate de securitate în proiectul RustDesk, te rugăm să o raportezi responsabil trimițând un e-mail la info@rustdesk.com. + +ÃŽn acest moment, nu avem un program de recompense pentru descoperirea de bug-uri. Suntem o echipă mică care încearcă să rezolve o problemă mare. +Te rugăm să raportezi orice vulnerabilitate în mod responsabil, astfel încât să putem continua să construim o aplicaÈ›ie sigură pentru întreaga comunitate. diff --git a/shelled/rustdesk-as-ref/docs/SECURITY-TR.md b/shelled/rustdesk-as-ref/docs/SECURITY-TR.md new file mode 100644 index 0000000..88037ac --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/SECURITY-TR.md @@ -0,0 +1,9 @@ +# Güvenlik Politikası + +## Bir Güvenlik Açığı Bildirme + +Projemiz için güvenliÄŸi çok önemsiyoruz. Kullanıcıların keÅŸfettikleri herhangi bir güvenlik açığını bize bildirmelerini teÅŸvik ediyoruz. +EÄŸer RustDesk projesinde bir güvenlik açığı bulursanız, lütfen info@rustdesk.com adresine sorumlu bir ÅŸekilde bildirin. + +Åžu an için bir hata ödül programımız bulunmamaktadır. Büyük bir sorunu çözmeye çalışan küçük bir ekibiz. Herhangi bir güvenlik açığını sorumlu bir ÅŸekilde bildirmenizi rica ederiz, +böylece tüm topluluk için güvenli bir uygulama oluÅŸturmaya devam edebiliriz. diff --git a/shelled/rustdesk-as-ref/docs/SECURITY.md b/shelled/rustdesk-as-ref/docs/SECURITY.md new file mode 100644 index 0000000..c595885 --- /dev/null +++ b/shelled/rustdesk-as-ref/docs/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a Vulnerability + +We value security for the project very highly. We encourage all users to report any vulnerabilities they discover to us. +If you find a security vulnerability in the RustDesk project, please report it responsibly by sending an email to info@rustdesk.com. + +At this juncture, we don't have a bug bounty program. We are a small team trying to solve a big problem. We urge you to report any vulnerabilities responsibly +so that we can continue building a secure application for the entire community. diff --git a/shelled/rustdesk-as-ref/entrypoint.sh b/shelled/rustdesk-as-ref/entrypoint.sh new file mode 100644 index 0000000..8c7be07 --- /dev/null +++ b/shelled/rustdesk-as-ref/entrypoint.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +cd "$HOME"/rustdesk || exit 1 +# shellcheck source=/dev/null +. "$HOME"/.cargo/env + +argv=$* + +while test $# -gt 0; do + case "$1" in + --release) + mkdir -p target/release + test -f target/release/libsciter-gtk.so || cp "$HOME"/libsciter-gtk.so target/release/ + release=1 + shift + ;; + --target) + shift + if test $# -gt 0; then + rustup target add "$1" + shift + fi + ;; + *) + shift + ;; + esac +done + +if [ -z $release ]; then + mkdir -p target/debug + test -f target/debug/libsciter-gtk.so || cp "$HOME"/libsciter-gtk.so target/debug/ +fi +set -f +#shellcheck disable=2086 +VCPKG_ROOT=/vcpkg cargo build $argv diff --git a/shelled/rustdesk-as-ref/examples/ipc.rs b/shelled/rustdesk-as-ref/examples/ipc.rs new file mode 100644 index 0000000..bca2321 --- /dev/null +++ b/shelled/rustdesk-as-ref/examples/ipc.rs @@ -0,0 +1,90 @@ +use docopt::Docopt; +use hbb_common::{ + env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV}, + log, tokio, +}; +use librustdesk::{ipc::Data, *}; + +const USAGE: &'static str = " +IPC test program. + +Usage: + ipc (-s | --server | -c | --client) [-p | --postfix=] + ipc (-h | --help) + +Options: + -h --help Show this screen. + -s --server Run as IPC server. + -c --client Run as IPC client. + -p --postfix= IPC path postfix [default: ]. +"; + +#[derive(Debug, serde::Deserialize)] +struct Args { + flag_server: bool, + flag_client: bool, + flag_postfix: String, +} + +#[tokio::main] +async fn main() { + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.deserialize()) + .unwrap_or_else(|e| e.exit()); + + if args.flag_server { + if args.flag_postfix.is_empty() { + log::info!("Starting IPC server..."); + } else { + log::info!( + "Starting IPC server with postfix: '{}'...", + args.flag_postfix + ); + } + ipc_server(&args.flag_postfix).await; + } else if args.flag_client { + if args.flag_postfix.is_empty() { + log::info!("Starting IPC client..."); + } else { + log::info!( + "Starting IPC client with postfix: '{}'...", + args.flag_postfix + ); + } + ipc_client(&args.flag_postfix).await; + } +} + +async fn ipc_server(postfix: &str) { + let postfix = postfix.to_string(); + let postfix2 = postfix.clone(); + std::thread::spawn(move || { + if let Err(err) = crate::ipc::start(&postfix) { + log::error!("Failed to start ipc: {}", err); + std::process::exit(-1); + } + }); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + ipc_client(&postfix2).await; +} + +async fn ipc_client(postfix: &str) { + loop { + match crate::ipc::connect(1000, postfix).await { + Ok(mut conn) => match conn.send(&Data::Empty).await { + Ok(_) => { + log::info!("send message to ipc server success"); + } + Err(e) => { + log::error!("Failed to send message to ipc server: {}", e); + } + }, + Err(e) => { + log::error!("Failed to connect to ipc server: {}", e); + } + } + tokio::time::sleep(std::time::Duration::from_secs(6)).await; + } +} diff --git a/shelled/rustdesk-as-ref/fastlane/metadata/android/en-US/full_description.txt b/shelled/rustdesk-as-ref/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 0000000..f78b3a2 --- /dev/null +++ b/shelled/rustdesk-as-ref/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,11 @@ +An open-source remote desktop application, the open source TeamViewer alternative. +Source code: https://github.com/rustdesk/rustdesk +Doc: https://rustdesk.com/docs/en/manual/mobile/ + +In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Android remote control. + +In addition to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. + +You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, or self-hosting, or write your own rendezvous/relay server. Self-hosting server is free and open source: https://github.com/rustdesk/rustdesk-server + +Please download and install desktop version from: https://rustdesk.com, then you can access and control your desktop from your mobile, or control your mobile from desktop. diff --git a/shelled/rustdesk-as-ref/fastlane/metadata/android/en-US/short_description.txt b/shelled/rustdesk-as-ref/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 0000000..91e796a --- /dev/null +++ b/shelled/rustdesk-as-ref/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +An open-source remote desktop application, the TeamViewer alternative diff --git a/shelled/rustdesk-as-ref/fastlane/metadata/android/fr-FR/full_description.txt b/shelled/rustdesk-as-ref/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 0000000..effb820 --- /dev/null +++ b/shelled/rustdesk-as-ref/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,11 @@ +Une application de bureau à distance open source, l'alternative open source à TeamViewer. +Code source : https://github.com/rustdesk/rustdesk +Doc : https://rustdesk.com/docs/en/manual/mobile/ + +Pour qu'un appareil distant puisse contrôler votre appareil Android via la souris ou le toucher, vous devez autoriser RustDesk à utiliser le service "Accessibilité", RustDesk utilise l'API AccessibilityService pour implémenter la télécommande Addroid. + +En plus du contrôle à distance, vous pouvez également transférer facilement des fichiers entre des appareils Android et des PC avec RustDesk. + +Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, ou l'auto-hébergement, ou écrire votre propre serveur de rendez-vous/relais. Le serveur auto-hébergé est gratuit et open source : https://github.com/rustdesk/rustdesk-server + +Veuillez télécharger et installer la version de bureau à partir de : https://rustdesk.com, vous pourrez alors accéder et contrôler votre bureau à partir de votre mobile, ou contrôler votre mobile à partir du bureau. diff --git a/shelled/rustdesk-as-ref/fastlane/metadata/android/fr-FR/short_description.txt b/shelled/rustdesk-as-ref/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 0000000..e1f4b4b --- /dev/null +++ b/shelled/rustdesk-as-ref/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Une application de bureau à distance open source, l'alternative open source à TeamViewer. diff --git a/shelled/rustdesk-as-ref/fastlane/metadata/android/nl-NL/full_description.txt b/shelled/rustdesk-as-ref/fastlane/metadata/android/nl-NL/full_description.txt new file mode 100644 index 0000000..b60d52c --- /dev/null +++ b/shelled/rustdesk-as-ref/fastlane/metadata/android/nl-NL/full_description.txt @@ -0,0 +1,11 @@ +Een open-source toepassing voor bureaublad op afstand, het open-source alternatief voor TeamViewer. +Bron code: https://github.com/rustdesk/rustdesk +Doc: https://rustdesk.com/docs/en/manual/mobile/ + +Om ervoor te zorgen dat een extern apparaat uw Android-apparaat via muis of aanraking kan besturen, moet u RustDesk toestaan de "Toegankelijkheid" service te gebruiken. RustDesk gebruikt AccessibilityService API om Android afstandsbediening te kunnen implementeren. + +Naast bediening op afstand kunt u met RustDesk ook eenvoudig bestanden overzetten tussen Android-apparaten en pc's. + +U hebt volledige controle over uw gegevens, en u hoeft zich geen zorgen te maken over de veiligheid. U kunt onze rendez-vous/relay server gebruiken, of zelf hosten, of uw eigen rendez-vous/relay server schrijven. Self-hosting server is gratis en open source: https://github.com/rustdesk/rustdesk-server + +Download en installeer de desktop versie vanaf: https://rustdesk.com, dan kunt u uw desktop benaderen en bedienen vanaf uw mobiel, of uw mobiel bedienen vanaf uw desktop. diff --git a/shelled/rustdesk-as-ref/fastlane/metadata/android/nl-NL/short_description.txt b/shelled/rustdesk-as-ref/fastlane/metadata/android/nl-NL/short_description.txt new file mode 100644 index 0000000..18a4600 --- /dev/null +++ b/shelled/rustdesk-as-ref/fastlane/metadata/android/nl-NL/short_description.txt @@ -0,0 +1 @@ +Een open-source toepassing voor bureaublad op afstand, het open-source alternatief voor TeamViewer. diff --git a/shelled/rustdesk-as-ref/fastlane/metadata/android/zh-CN/full_description.txt b/shelled/rustdesk-as-ref/fastlane/metadata/android/zh-CN/full_description.txt new file mode 100644 index 0000000..f1f4405 --- /dev/null +++ b/shelled/rustdesk-as-ref/fastlane/metadata/android/zh-CN/full_description.txt @@ -0,0 +1,12 @@ +å¼€æºè¿œç¨‹æ¡Œé¢åº”ç”¨ï¼Œå¼€æº TeamViewer 替代方案。 +æºä»£ç ï¼šhttps://github.com/rustdesk/rustdesk +文档:https://rustdesk.com/docs/en/manual/mobile/ + +为了让远程设备通过鼠标或触摸控制您的 Android 设备,您需è¦å…许 RustDesk 使用“Accessibilityâ€æœåŠ¡ï¼ŒRustDesk 使用 AccessibilityService API æ¥å®žçް Addroid 远程控制。 + +除了远程控制,您还å¯ä»¥ä½¿ç”¨ RustDesk 在 Android 设备和 PC 之间轻æ¾ä¼ è¾“文件。 + +您完全掌控数æ®ï¼Œä¸ç”¨æ‹…心安全问题。您å¯ä»¥ä½¿ç”¨æˆ‘们的注册/中继æœåŠ¡å™¨ï¼Œæˆ–è€…è‡ªå»ºï¼Œäº¦æˆ–è€…å¼€å‘æ‚¨çš„版本。 +自托管æœåŠ¡å™¨æ˜¯å…费和开æºçš„:https://github.com/rustdesk/rustdesk-server + +请从:https://rustdesk.com 下载并安装桌é¢ç‰ˆï¼Œç„¶åŽæ‚¨å¯ä»¥é€šè¿‡æ‰‹æœºè®¿é—®å’ŒæŽ§åˆ¶æ‚¨çš„æ¡Œé¢ï¼Œæˆ–ä»Žæ¡Œé¢æŽ§åˆ¶æ‚¨çš„æ‰‹æœºã€‚ diff --git a/shelled/rustdesk-as-ref/fastlane/metadata/android/zh-CN/short_description.txt b/shelled/rustdesk-as-ref/fastlane/metadata/android/zh-CN/short_description.txt new file mode 100644 index 0000000..69a4a5b --- /dev/null +++ b/shelled/rustdesk-as-ref/fastlane/metadata/android/zh-CN/short_description.txt @@ -0,0 +1 @@ +å¼€æºè¿œç¨‹æ¡Œé¢åº”ç”¨ï¼Œå¼€æº TeamViewer 替代方案 diff --git a/shelled/rustdesk-as-ref/flatpak/com.rustdesk.RustDesk.metainfo.xml b/shelled/rustdesk-as-ref/flatpak/com.rustdesk.RustDesk.metainfo.xml new file mode 100644 index 0000000..0d3b33b --- /dev/null +++ b/shelled/rustdesk-as-ref/flatpak/com.rustdesk.RustDesk.metainfo.xml @@ -0,0 +1,59 @@ + + + com.rustdesk.RustDesk + + RustDesk + + com.rustdesk.RustDesk.desktop + CC0-1.0 + AGPL-3.0-only + RustDesk + Secure remote desktop access + +

+ RustDesk is a full-featured open source remote control alternative for self-hosting and security with minimal configuration. +

+
    +
  • Works on Windows, macOS, Linux, iOS, Android, Web.
  • +
  • Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs.
  • +
  • Own your data, easily set up self-hosting solution on your infrastructure.
  • +
  • P2P connection with end-to-end encryption based on NaCl.
  • +
  • No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand.
  • +
  • We like to keep things simple and will strive to make simpler where possible.
  • +
+

+ For self-hosting setup instructions please go to our home page. +

+
+ + Utility + + + + Remote desktop session + https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png + + + + #d9eaf8 + #0160ee + + https://rustdesk.com + https://github.com/rustdesk/rustdesk/issues + https://github.com/rustdesk/rustdesk/wiki/FAQ + https://rustdesk.com/docs + https://ko-fi.com/rustdesk + https://github.com/rustdesk/rustdesk + https://github.com/rustdesk/rustdesk/tree/master/src/lang + https://github.com/rustdesk/rustdesk/blob/master/docs/CONTRIBUTING.md + https://rustdesk.com/docs/en/technical-support + + 600 + always + + + keyboard + pointing + + +
\ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flatpak/rustdesk.json b/shelled/rustdesk-as-ref/flatpak/rustdesk.json new file mode 100644 index 0000000..2418ac2 --- /dev/null +++ b/shelled/rustdesk-as-ref/flatpak/rustdesk.json @@ -0,0 +1,66 @@ +{ + "id": "com.rustdesk.RustDesk", + "runtime": "org.freedesktop.Platform", + "runtime-version": "24.08", + "sdk": "org.freedesktop.Sdk", + "command": "rustdesk", + "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"], + "rename-desktop-file": "rustdesk.desktop", + "rename-icon": "rustdesk", + "modules": [ + "shared-modules/libappindicator/libappindicator-gtk3-12.10.json", + { + "name": "xdotool", + "no-autogen": true, + "make-install-args": ["PREFIX=${FLATPAK_DEST}"], + "sources": [ + { + "type": "archive", + "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", + "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" + } + ] + }, + { + "name": "pam", + "buildsystem": "autotools", + "config-opts": ["--disable-selinux"], + "sources": [ + { + "type": "archive", + "url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz", + "sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db" + } + ] + }, + { + "name": "rustdesk", + "buildsystem": "simple", + "build-commands": [ + "bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -", + "cp -r usr/* /app/", + "mkdir -p /app/bin && ln -s /app/share/rustdesk/rustdesk /app/bin/rustdesk" + ], + "sources": [ + { + "type": "file", + "path": "rustdesk.deb" + }, + { + "type": "file", + "path": "com.rustdesk.RustDesk.metainfo.xml" + } + ] + } + ], + "finish-args": [ + "--share=ipc", + "--socket=wayland", + "--socket=x11", + "--share=network", + "--filesystem=home", + "--device=dri", + "--socket=pulseaudio", + "--talk-name=org.freedesktop.Flatpak" + ] +} \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/.gitattributes b/shelled/rustdesk-as-ref/flutter/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/shelled/rustdesk-as-ref/flutter/.gitignore b/shelled/rustdesk-as-ref/flutter/.gitignore new file mode 100644 index 0000000..ee7e42c --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/.gitignore @@ -0,0 +1,56 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related + +# Symbolication related +app.*.symbols +# Obfuscation related +app.*.map.json +jniLibs +.vscode + +# flutter rust bridge +lib/generated_bridge.dart +lib/generated_bridge.freezed.dart + +# Flutter Generated Files +**/GeneratedPluginRegistrant.swift +**/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 +macos/rustdesk.xcodeproj/project.xcworkspace/ diff --git a/shelled/rustdesk-as-ref/flutter/.metadata b/shelled/rustdesk-as-ref/flutter/.metadata new file mode 100644 index 0000000..8b4892c --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/.metadata @@ -0,0 +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. + +version: + 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/shelled/rustdesk-as-ref/flutter/README.md b/shelled/rustdesk-as-ref/flutter/README.md new file mode 100644 index 0000000..519b85e --- /dev/null +++ b/shelled/rustdesk-as-ref/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 and guidance on mobile development, and a full API reference. diff --git a/shelled/rustdesk-as-ref/flutter/analysis_options.yaml b/shelled/rustdesk-as-ref/flutter/analysis_options.yaml new file mode 100644 index 0000000..a679f57 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:lints/recommended.yaml + +linter: + rules: + non_constant_identifier_names: false + sort_child_properties_last: false diff --git a/shelled/rustdesk-as-ref/flutter/android/.gitignore b/shelled/rustdesk-as-ref/flutter/android/.gitignore new file mode 100644 index 0000000..0a741cb --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/shelled/rustdesk-as-ref/flutter/android/app/build.gradle b/shelled/rustdesk-as-ref/flutter/android/app/build.gradle new file mode 100644 index 0000000..830cbc2 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/build.gradle @@ -0,0 +1,137 @@ +import com.google.protobuf.gradle.* +import groovy.json.JsonSlurper + +plugins { + id "com.google.protobuf" version "0.9.4" + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +// Add rustls-platform-verifier Android support +String findRustlsPlatformVerifierMavenDir() { + def dependencyText = providers.exec { + it.workingDir = new File("../..") + commandLine("cargo", "metadata", "--format-version", "1") + }.standardOutput.asText.get() + + def dependencyJson = new JsonSlurper().parseText(dependencyText) + def pkg = dependencyJson.packages.find { it.name == "rustls-platform-verifier-android" } + + if (pkg == null) { + throw new GradleException("rustls-platform-verifier-android package not found in cargo metadata!") + } + + def manifestPath = file(pkg.manifest_path) + def mavenDir = new File(manifestPath.parentFile, "maven") + + if (!mavenDir.exists()) { + throw new GradleException("Maven directory not found at: ${mavenDir.path}") + } + + println("✓ Found rustls-platform-verifier maven repo at: ${mavenDir.path}") + return mavenDir.path +} + + +repositories { + maven { + url = findRustlsPlatformVerifierMavenDir() + metadataSources.artifact() + } +} + +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:3.20.1' + } + + generateProtoTasks { + all().configureEach { task -> + task.builtins { + java { + option "lite" + } + } + } + } +} + +android { + compileSdkVersion 34 + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + + main.proto.srcDirs += '../../../libs/hbb_common/protos' + main.proto.includes += "message.proto" + } + + compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.carriez.flutter_hbb" + minSdkVersion 22 + targetSdkVersion 33 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.release + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules' + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation 'com.google.protobuf:protobuf-javalite:3.20.1' + implementation "androidx.media:media:1.6.0" + implementation 'com.github.getActivity:XXPermissions:18.5' + implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("1.9.10") } } + implementation 'com.caverock:androidsvg-aar:1.4' + implementation "rustls:rustls-platform-verifier:0.1.1" +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/proguard-rules b/shelled/rustdesk-as-ref/flutter/android/app/proguard-rules new file mode 100644 index 0000000..5174025 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/proguard-rules @@ -0,0 +1,7 @@ +# Keep class members from protobuf generated code. +-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { + ; +} + +# Keep rustls-platform-verifier classes for JNI +-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; } \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/debug/AndroidManifest.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..64d68a5 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/AndroidManifest.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f4788af --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt new file mode 100644 index 0000000..db222dc --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt @@ -0,0 +1,193 @@ +package com.carriez.flutter_hbb + +import ffi.FFI + +import android.Manifest +import android.content.Context +import android.media.* +import android.content.pm.PackageManager +import android.media.projection.MediaProjection +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import android.os.Build +import android.util.Log +import kotlin.concurrent.thread + +const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30 +const val AUDIO_SAMPLE_RATE = 48000 +const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO + +class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) { + private val logTag = "LOG_AUDIO_RECORD_HANDLE" + + private var audioRecorder: AudioRecord? = null + private var audioReader: AudioReader? = null + private var minBufferSize = 0 + private var audioRecordStat = false + private var audioThread: Thread? = null + + @RequiresApi(Build.VERSION_CODES.M) + fun createAudioRecorder(inVoiceCall: Boolean, mediaProjection: MediaProjection?): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return false + } + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.RECORD_AUDIO + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.d(logTag, "createAudioRecorder failed, no RECORD_AUDIO permission") + return false + } + + var builder = AudioRecord.Builder() + .setAudioFormat( + AudioFormat.Builder() + .setEncoding(AUDIO_ENCODING) + .setSampleRate(AUDIO_SAMPLE_RATE) + .setChannelMask(AUDIO_CHANNEL_MASK).build() + ); + if (inVoiceCall) { + builder.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) + } else { + mediaProjection?.let { + var apcc = AudioPlaybackCaptureConfiguration.Builder(it) + .addMatchingUsage(AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(AudioAttributes.USAGE_ALARM) + .addMatchingUsage(AudioAttributes.USAGE_GAME) + .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build(); + builder.setAudioPlaybackCaptureConfig(apcc); + } ?: let { + Log.d(logTag, "createAudioRecorder failed, mediaProjection null") + return false + } + } + audioRecorder = builder.build() + Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize") + return true + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun checkAudioReader() { + if (audioReader != null && minBufferSize != 0) { + return + } + // read f32 to byte , length * 4 + minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize( + AUDIO_SAMPLE_RATE, + AUDIO_CHANNEL_MASK, + AUDIO_ENCODING + ) + if (minBufferSize == 0) { + Log.d(logTag, "get min buffer size fail!") + return + } + audioReader = AudioReader(minBufferSize, 4) + Log.d(logTag, "init audioData len:$minBufferSize") + } + + @RequiresApi(Build.VERSION_CODES.M) + fun startAudioRecorder() { + checkAudioReader() + if (audioReader != null && audioRecorder != null && minBufferSize != 0) { + try { + FFI.setFrameRawEnable("audio", true) + audioRecorder!!.startRecording() + audioRecordStat = true + audioThread = thread { + while (audioRecordStat) { + audioReader!!.readSync(audioRecorder!!)?.let { + FFI.onAudioFrameUpdate(it) + } + } + // let's release here rather than onDestroy to avoid threading issue + audioRecorder?.release() + audioRecorder = null + minBufferSize = 0 + FFI.setFrameRawEnable("audio", false) + Log.d(logTag, "Exit audio thread") + } + } catch (e: Exception) { + Log.d(logTag, "startAudioRecorder fail:$e") + } + } else { + Log.d(logTag, "startAudioRecorder fail") + } + } + + fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean { + if (!isSupportVoiceCall()) { + return false + } + // No need to check if video or audio is started here. + if (!switchToVoiceCall(mediaProjection)) { + return false + } + return true + } + + fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean { + // Return true if not supported, because is was not started. + if (!isSupportVoiceCall()) { + return true + } + if (isVideoStart()) { + switchOutVoiceCall(mediaProjection) + } + tryReleaseAudio() + return true + } + + @RequiresApi(Build.VERSION_CODES.M) + fun switchToVoiceCall(mediaProjection: MediaProjection?): Boolean { + audioRecorder?.let { + if (it.getAudioSource() == MediaRecorder.AudioSource.VOICE_COMMUNICATION) { + return true + } + } + audioRecordStat = false + audioThread?.join() + audioThread = null + + if (!createAudioRecorder(true, mediaProjection)) { + Log.e(logTag, "createAudioRecorder fail") + return false + } + startAudioRecorder() + return true + } + + @RequiresApi(Build.VERSION_CODES.M) + fun switchOutVoiceCall(mediaProjection: MediaProjection?): Boolean { + audioRecorder?.let { + if (it.getAudioSource() != MediaRecorder.AudioSource.VOICE_COMMUNICATION) { + return true + } + } + audioRecordStat = false + audioThread?.join() + + if (!createAudioRecorder(false, mediaProjection)) { + Log.e(logTag, "createAudioRecorder fail") + return false + } + startAudioRecorder() + return true + } + + fun tryReleaseAudio() { + if (isAudioStart() || isVideoStart()) { + return + } + audioRecordStat = false + audioThread?.join() + audioThread = null + } + + fun destroy() { + Log.d(logTag, "destroy audio record handle") + + audioRecordStat = false + audioThread?.join() + } +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt new file mode 100644 index 0000000..71bbba7 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt @@ -0,0 +1,47 @@ +package com.carriez.flutter_hbb + +import android.Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +import android.Manifest.permission.SYSTEM_ALERT_WINDOW +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import android.widget.Toast +import com.hjq.permissions.XXPermissions +import io.flutter.embedding.android.FlutterActivity + +const val DEBUG_BOOT_COMPLETED = "com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED" + +class BootReceiver : BroadcastReceiver() { + private val logTag = "tagBootReceiver" + + override fun onReceive(context: Context, intent: Intent) { + Log.d(logTag, "onReceive ${intent.action}") + + if (Intent.ACTION_BOOT_COMPLETED == intent.action || DEBUG_BOOT_COMPLETED == intent.action) { + // check SharedPreferences config + val prefs = context.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE) + if (!prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) { + Log.d(logTag, "KEY_START_ON_BOOT_OPT is false") + return + } + // check pre-permission + if (!XXPermissions.isGranted(context, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, SYSTEM_ALERT_WINDOW)){ + Log.d(logTag, "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS or SYSTEM_ALERT_WINDOW is not granted") + return + } + + val it = Intent(context, MainService::class.java).apply { + action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE + putExtra(EXT_INIT_FROM_BOOT, true) + } + Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(it) + } else { + context.startService(it) + } + } + } +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt new file mode 100644 index 0000000..6dd4a2f --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt @@ -0,0 +1,394 @@ +package com.carriez.flutter_hbb + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.PixelFormat +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.util.Log +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN +import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE +import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL +import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON +import android.widget.ImageView +import android.widget.PopupMenu +import com.caverock.androidsvg.SVG +import ffi.FFI +import kotlin.math.abs + +class FloatingWindowService : Service(), View.OnTouchListener { + + private lateinit var windowManager: WindowManager + private lateinit var layoutParams: WindowManager.LayoutParams + private lateinit var floatingView: ImageView + private lateinit var originalDrawable: Drawable + private lateinit var leftHalfDrawable: Drawable + private lateinit var rightHalfDrawable: Drawable + + private var dragging = false + private var lastDownX = 0f + private var lastDownY = 0f + private var viewCreated = false; + private var keepScreenOn = KeepScreenOn.DURING_CONTROLLED + + companion object { + private val logTag = "floatingService" + private var firstCreate = true + private var viewWidth = 120 + private var viewHeight = 120 + private const val MIN_VIEW_SIZE = 32 // size 0 does not help prevent the service from being killed + private const val MAX_VIEW_SIZE = 320 + private var viewUntouchable = false + private var viewTransparency = 1f // 0 means invisible but can help prevent the service from being killed + private var customSvg = "" + private var lastLayoutX = 0 + private var lastLayoutY = 0 + private var lastOrientation = Configuration.ORIENTATION_UNDEFINED + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onCreate() { + super.onCreate() + windowManager = getSystemService(WINDOW_SERVICE) as WindowManager + try { + if (firstCreate) { + firstCreate = false + onFirstCreate(windowManager) + } + Log.d(logTag, "floating window size: $viewWidth x $viewHeight, transparency: $viewTransparency, lastLayoutX: $lastLayoutX, lastLayoutY: $lastLayoutY, customSvg: $customSvg") + createView(windowManager) + handler.postDelayed(runnable, 1000) + Log.d(logTag, "onCreate success") + } catch (e: Exception) { + Log.d(logTag, "onCreate failed: $e") + } + } + + override fun onDestroy() { + super.onDestroy() + if (viewCreated) { + windowManager.removeView(floatingView) + } + handler.removeCallbacks(runnable) + } + + @SuppressLint("ClickableViewAccessibility") + private fun createView(windowManager: WindowManager) { + floatingView = ImageView(this) + viewCreated = true + originalDrawable = resources.getDrawable(R.drawable.floating_window, null) + if (customSvg.isNotEmpty()) { + try { + val svg = SVG.getFromString(customSvg) + Log.d(logTag, "custom svg info: ${svg.documentWidth} x ${svg.documentHeight}"); + // This make the svg render clear + svg.documentWidth = viewWidth * 1f + svg.documentHeight = viewHeight * 1f + originalDrawable = svg.renderToPicture().let { + BitmapDrawable( + resources, + Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888) + .also { bitmap -> + it.draw(Canvas(bitmap)) + }) + } + floatingView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + Log.d(logTag, "custom svg loaded") + } catch (e: Exception) { + e.printStackTrace() + } + } + val originalBitmap = Bitmap.createBitmap( + originalDrawable.intrinsicWidth, + originalDrawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(originalBitmap) + originalDrawable.setBounds( + 0, + 0, + originalDrawable.intrinsicWidth, + originalDrawable.intrinsicHeight + ) + originalDrawable.draw(canvas) + val leftHalfBitmap = Bitmap.createBitmap( + originalBitmap, + 0, + 0, + originalDrawable.intrinsicWidth / 2, + originalDrawable.intrinsicHeight + ) + val rightHalfBitmap = Bitmap.createBitmap( + originalBitmap, + originalDrawable.intrinsicWidth / 2, + 0, + originalDrawable.intrinsicWidth / 2, + originalDrawable.intrinsicHeight + ) + leftHalfDrawable = BitmapDrawable(resources, leftHalfBitmap) + rightHalfDrawable = BitmapDrawable(resources, rightHalfBitmap) + + floatingView.setImageDrawable(rightHalfDrawable) + floatingView.setOnTouchListener(this) + floatingView.alpha = viewTransparency * 1f + + var flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL or FLAG_NOT_FOCUSABLE + if (viewUntouchable || viewTransparency == 0f) { + flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + } + layoutParams = WindowManager.LayoutParams( + viewWidth / 2, + viewHeight, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE, + flags, + PixelFormat.TRANSLUCENT + ) + + layoutParams.gravity = Gravity.TOP or Gravity.START + layoutParams.x = lastLayoutX + layoutParams.y = lastLayoutY + + val keepScreenOnOption = FFI.getLocalOption("keep-screen-on").lowercase() + keepScreenOn = when (keepScreenOnOption) { + "never" -> KeepScreenOn.NEVER + "service-on" -> KeepScreenOn.SERVICE_ON + else -> KeepScreenOn.DURING_CONTROLLED + } + Log.d(logTag, "keepScreenOn option: $keepScreenOnOption, value: $keepScreenOn") + updateKeepScreenOnLayoutParams() + + windowManager.addView(floatingView, layoutParams) + moveToScreenSide() + } + + private fun onFirstCreate(windowManager: WindowManager) { + val wh = getScreenSize(windowManager) + val w = wh.first + val h = wh.second + // size + FFI.getLocalOption("floating-window-size").let { + if (it.isNotEmpty()) { + try { + val size = it.toInt() + if (size in MIN_VIEW_SIZE..MAX_VIEW_SIZE && size <= w / 2 && size <= h / 2) { + viewWidth = size + viewHeight = size + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + // untouchable + viewUntouchable = FFI.getLocalOption("floating-window-untouchable") == "Y" + // transparency + FFI.getLocalOption("floating-window-transparency").let { + if (it.isNotEmpty()) { + try { + val transparency = it.toInt() + if (transparency in 0..10) { + viewTransparency = transparency * 1f / 10 + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + // custom svg + FFI.getLocalOption("floating-window-svg").let { + if (it.isNotEmpty()) { + customSvg = it + } + } + // position + lastLayoutX = 0 + lastLayoutY = (wh.second - viewHeight) / 2 + lastOrientation = resources.configuration.orientation + } + + + + private fun performClick() { + showPopupMenu() + } + + override fun onTouch(view: View?, event: MotionEvent?): Boolean { + when (event?.action) { + MotionEvent.ACTION_DOWN -> { + dragging = false + lastDownX = event.rawX + lastDownY = event.rawY + } + MotionEvent.ACTION_UP -> { + val clickDragTolerance = 10f + if (abs(event.rawX - lastDownX) < clickDragTolerance && abs(event.rawY - lastDownY) < clickDragTolerance) { + performClick() + } else { + moveToScreenSide() + } + } + MotionEvent.ACTION_MOVE -> { + val dx = event.rawX - lastDownX + val dy = event.rawY - lastDownY + // ignore too small fist start moving(some time is click) + if (!dragging && dx*dx+dy*dy < 25) { + return false + } + dragging = true + layoutParams.x = event.rawX.toInt() + layoutParams.y = event.rawY.toInt() + layoutParams.width = viewWidth + floatingView.setImageDrawable(originalDrawable) + windowManager.updateViewLayout(view, layoutParams) + lastLayoutX = layoutParams.x + lastLayoutY = layoutParams.y + } + } + return false + } + + private fun moveToScreenSide(center: Boolean = false) { + val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager + val wh = getScreenSize(windowManager) + val w = wh.first + if (layoutParams.x < w / 2) { + layoutParams.x = 0 + floatingView.setImageDrawable(rightHalfDrawable) + } else { + layoutParams.x = w - viewWidth / 2 + floatingView.setImageDrawable(leftHalfDrawable) + } + if (center) { + layoutParams.y = (wh.second - viewHeight) / 2 + } + layoutParams.width = viewWidth / 2 + windowManager.updateViewLayout(floatingView, layoutParams) + lastLayoutX = layoutParams.x + lastLayoutY = layoutParams.y + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + if (newConfig.orientation != lastOrientation) { + lastOrientation = newConfig.orientation + val wh = getScreenSize(windowManager) + Log.d(logTag, "orientation: $lastOrientation, screen size: ${wh.first} x ${wh.second}") + val newW = wh.first + val newH = wh.second + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE || newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { + // Proportional change + layoutParams.x = (layoutParams.x.toFloat() / newH.toFloat() * newW.toFloat()).toInt() + layoutParams.y = (layoutParams.y.toFloat() / newW.toFloat() * newH.toFloat()).toInt() + } + moveToScreenSide() + } + } + + private fun showPopupMenu() { + val popupMenu = PopupMenu(this, floatingView) + val idShowRustDesk = 0 + popupMenu.menu.add(0, idShowRustDesk, 0, translate("Show RustDesk")) + // For host side, clipboard sync + val idSyncClipboard = 1 + val isServiceSyncEnabled = (MainActivity.rdClipboardManager?.isCaptureStarted ?: false) && FFI.isServiceClipboardEnabled() + if (isServiceSyncEnabled) { + popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard")) + } + val idStopService = 2 + val hideStopService = FFI.getBuildinOption("hide-stop-service") == "Y" + if (!hideStopService) { + popupMenu.menu.add(0, idStopService, 0, translate("Stop service")) + } + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + idShowRustDesk -> { + openMainActivity() + true + } + idSyncClipboard -> { + syncClipboard() + true + } + idStopService -> { + stopMainService() + true + } + else -> false + } + } + popupMenu.setOnDismissListener { + moveToScreenSide() + } + popupMenu.show() + } + + + private fun openMainActivity() { + val intent = Intent(this, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT + ) + try { + pendingIntent.send() + } catch (e: PendingIntent.CanceledException) { + e.printStackTrace() + } + } + + private fun syncClipboard() { + MainActivity.rdClipboardManager?.syncClipboard(false) + } + + private fun stopMainService() { + MainActivity.flutterMethodChannel?.invokeMethod("stop_service", null) + } + + enum class KeepScreenOn { + NEVER, + DURING_CONTROLLED, + SERVICE_ON, + } + + private val handler = Handler(Looper.getMainLooper()) + private val runnable = object : Runnable { + override fun run() { + if (updateKeepScreenOnLayoutParams()) { + windowManager.updateViewLayout(floatingView, layoutParams) + } + handler.postDelayed(this, 1000) // 1000 milliseconds = 1 second + } + } + + private fun updateKeepScreenOnLayoutParams(): Boolean { + val oldOn = layoutParams.flags and FLAG_KEEP_SCREEN_ON != 0 + val newOn = keepScreenOn == KeepScreenOn.SERVICE_ON || (keepScreenOn == KeepScreenOn.DURING_CONTROLLED && MainService.isStart) + if (oldOn != newOn) { + Log.d(logTag, "change keep screen on to $newOn") + if (newOn) { + layoutParams.flags = layoutParams.flags or FLAG_KEEP_SCREEN_ON + } else { + layoutParams.flags = layoutParams.flags and FLAG_KEEP_SCREEN_ON.inv() + } + return true + } + return false + } +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt new file mode 100644 index 0000000..3ca83fb --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt @@ -0,0 +1,741 @@ +package com.carriez.flutter_hbb + +/** + * Handle remote input and dispatch android gesture + * + * Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG + */ + +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.GestureDescription +import android.graphics.Path +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.widget.EditText +import android.view.accessibility.AccessibilityEvent +import android.view.ViewGroup.LayoutParams +import android.view.accessibility.AccessibilityNodeInfo +import android.view.KeyEvent as KeyEventAndroid +import android.view.ViewConfiguration +import android.graphics.Rect +import android.media.AudioManager +import android.accessibilityservice.AccessibilityServiceInfo +import android.accessibilityservice.AccessibilityServiceInfo.FLAG_INPUT_METHOD_EDITOR +import android.accessibilityservice.AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS +import android.view.inputmethod.EditorInfo +import androidx.annotation.RequiresApi +import java.util.* +import java.lang.Character +import kotlin.math.abs +import kotlin.math.max +import hbb.MessageOuterClass.KeyEvent +import hbb.MessageOuterClass.KeyboardMode +import hbb.KeyEventConverter + +// const val BUTTON_UP = 2 +// const val BUTTON_BACK = 0x08 + +const val LEFT_DOWN = 9 +const val LEFT_MOVE = 8 +const val LEFT_UP = 10 +const val RIGHT_UP = 18 +// (BUTTON_BACK << 3) | BUTTON_UP +const val BACK_UP = 66 +const val WHEEL_BUTTON_DOWN = 33 +const val WHEEL_BUTTON_UP = 34 +const val WHEEL_DOWN = 523331 +const val WHEEL_UP = 963 + +const val TOUCH_SCALE_START = 1 +const val TOUCH_SCALE = 2 +const val TOUCH_SCALE_END = 3 +const val TOUCH_PAN_START = 4 +const val TOUCH_PAN_UPDATE = 5 +const val TOUCH_PAN_END = 6 + +const val WHEEL_STEP = 120 +const val WHEEL_DURATION = 50L +const val LONG_TAP_DELAY = 200L + +class InputService : AccessibilityService() { + + companion object { + var ctx: InputService? = null + val isOpen: Boolean + get() = ctx != null + } + + private val logTag = "input service" + private var leftIsDown = false + private var touchPath = Path() + private var stroke: GestureDescription.StrokeDescription? = null + private var lastTouchGestureStartTime = 0L + private var mouseX = 0 + private var mouseY = 0 + private var timer = Timer() + private var recentActionTask: TimerTask? = null + // 100(tap timeout) + 400(long press timeout) + private val longPressDuration = ViewConfiguration.getTapTimeout().toLong() + ViewConfiguration.getLongPressTimeout().toLong() + + private val wheelActionsQueue = LinkedList() + private var isWheelActionsPolling = false + private var isWaitingLongPress = false + + private var fakeEditTextForTextStateCalculation: EditText? = null + + private var lastX = 0 + private var lastY = 0 + + private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) } + + @RequiresApi(Build.VERSION_CODES.N) + fun onMouseInput(mask: Int, _x: Int, _y: Int) { + val x = max(0, _x) + val y = max(0, _y) + + if (mask == 0 || mask == LEFT_MOVE) { + val oldX = mouseX + val oldY = mouseY + mouseX = x * SCREEN_INFO.scale + mouseY = y * SCREEN_INFO.scale + if (isWaitingLongPress) { + val delta = abs(oldX - mouseX) + abs(oldY - mouseY) + Log.d(logTag,"delta:$delta") + if (delta > 8) { + isWaitingLongPress = false + } + } + } + + // left button down, was up + if (mask == LEFT_DOWN) { + isWaitingLongPress = true + timer.schedule(object : TimerTask() { + override fun run() { + if (isWaitingLongPress) { + isWaitingLongPress = false + continueGesture(mouseX, mouseY) + } + } + }, longPressDuration) + + leftIsDown = true + startGesture(mouseX, mouseY) + return + } + + // left down, was down + if (leftIsDown) { + continueGesture(mouseX, mouseY) + } + + // left up, was down + if (mask == LEFT_UP) { + if (leftIsDown) { + leftIsDown = false + isWaitingLongPress = false + endGesture(mouseX, mouseY) + return + } + } + + if (mask == RIGHT_UP) { + longPress(mouseX, mouseY) + return + } + + if (mask == BACK_UP) { + performGlobalAction(GLOBAL_ACTION_BACK) + return + } + + // long WHEEL_BUTTON_DOWN -> GLOBAL_ACTION_RECENTS + if (mask == WHEEL_BUTTON_DOWN) { + timer.purge() + recentActionTask = object : TimerTask() { + override fun run() { + performGlobalAction(GLOBAL_ACTION_RECENTS) + recentActionTask = null + } + } + timer.schedule(recentActionTask, LONG_TAP_DELAY) + } + + // wheel button up + if (mask == WHEEL_BUTTON_UP) { + if (recentActionTask != null) { + recentActionTask!!.cancel() + performGlobalAction(GLOBAL_ACTION_HOME) + } + return + } + + if (mask == WHEEL_DOWN) { + if (mouseY < WHEEL_STEP) { + return + } + val path = Path() + path.moveTo(mouseX.toFloat(), mouseY.toFloat()) + path.lineTo(mouseX.toFloat(), (mouseY - WHEEL_STEP).toFloat()) + val stroke = GestureDescription.StrokeDescription( + path, + 0, + WHEEL_DURATION + ) + val builder = GestureDescription.Builder() + builder.addStroke(stroke) + wheelActionsQueue.offer(builder.build()) + consumeWheelActions() + + } + + if (mask == WHEEL_UP) { + if (mouseY < WHEEL_STEP) { + return + } + val path = Path() + path.moveTo(mouseX.toFloat(), mouseY.toFloat()) + path.lineTo(mouseX.toFloat(), (mouseY + WHEEL_STEP).toFloat()) + val stroke = GestureDescription.StrokeDescription( + path, + 0, + WHEEL_DURATION + ) + val builder = GestureDescription.Builder() + builder.addStroke(stroke) + wheelActionsQueue.offer(builder.build()) + consumeWheelActions() + } + } + + @RequiresApi(Build.VERSION_CODES.N) + fun onTouchInput(mask: Int, _x: Int, _y: Int) { + when (mask) { + TOUCH_PAN_UPDATE -> { + mouseX -= _x * SCREEN_INFO.scale + mouseY -= _y * SCREEN_INFO.scale + mouseX = max(0, mouseX); + mouseY = max(0, mouseY); + continueGesture(mouseX, mouseY) + } + TOUCH_PAN_START -> { + mouseX = max(0, _x) * SCREEN_INFO.scale + mouseY = max(0, _y) * SCREEN_INFO.scale + startGesture(mouseX, mouseY) + } + TOUCH_PAN_END -> { + endGesture(mouseX, mouseY) + mouseX = max(0, _x) * SCREEN_INFO.scale + mouseY = max(0, _y) * SCREEN_INFO.scale + } + else -> {} + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun consumeWheelActions() { + if (isWheelActionsPolling) { + return + } else { + isWheelActionsPolling = true + } + wheelActionsQueue.poll()?.let { + dispatchGesture(it, null, null) + timer.purge() + timer.schedule(object : TimerTask() { + override fun run() { + isWheelActionsPolling = false + consumeWheelActions() + } + }, WHEEL_DURATION + 10) + } ?: let { + isWheelActionsPolling = false + return + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun performClick(x: Int, y: Int, duration: Long) { + val path = Path() + path.moveTo(x.toFloat(), y.toFloat()) + try { + val longPressStroke = GestureDescription.StrokeDescription(path, 0, duration) + val builder = GestureDescription.Builder() + builder.addStroke(longPressStroke) + Log.d(logTag, "performClick x:$x y:$y time:$duration") + dispatchGesture(builder.build(), null, null) + } catch (e: Exception) { + Log.e(logTag, "performClick, error:$e") + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun longPress(x: Int, y: Int) { + performClick(x, y, longPressDuration) + } + + private fun startGesture(x: Int, y: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + touchPath.reset() + } else { + touchPath = Path() + } + touchPath.moveTo(x.toFloat(), y.toFloat()) + lastTouchGestureStartTime = System.currentTimeMillis() + lastX = x + lastY = y + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun doDispatchGesture(x: Int, y: Int, willContinue: Boolean) { + touchPath.lineTo(x.toFloat(), y.toFloat()) + var duration = System.currentTimeMillis() - lastTouchGestureStartTime + if (duration <= 0) { + duration = 1 + } + try { + if (stroke == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration, + willContinue + ) + } else { + stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration + ) + } + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue) + } else { + stroke = null + stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration + ) + } + } + stroke?.let { + val builder = GestureDescription.Builder() + builder.addStroke(it) + Log.d(logTag, "doDispatchGesture x:$x y:$y time:$duration") + dispatchGesture(builder.build(), null, null) + } + } catch (e: Exception) { + Log.e(logTag, "doDispatchGesture, willContinue:$willContinue, error:$e") + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun continueGesture(x: Int, y: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + doDispatchGesture(x, y, true) + touchPath.reset() + touchPath.moveTo(x.toFloat(), y.toFloat()) + lastTouchGestureStartTime = System.currentTimeMillis() + lastX = x + lastY = y + } else { + touchPath.lineTo(x.toFloat(), y.toFloat()) + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun endGestureBelowO(x: Int, y: Int) { + try { + touchPath.lineTo(x.toFloat(), y.toFloat()) + var duration = System.currentTimeMillis() - lastTouchGestureStartTime + if (duration <= 0) { + duration = 1 + } + val stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration + ) + val builder = GestureDescription.Builder() + builder.addStroke(stroke) + Log.d(logTag, "end gesture x:$x y:$y time:$duration") + dispatchGesture(builder.build(), null, null) + } catch (e: Exception) { + Log.e(logTag, "endGesture error:$e") + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun endGesture(x: Int, y: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + doDispatchGesture(x, y, false) + touchPath.reset() + stroke = null + } else { + endGestureBelowO(x, y) + } + } + + @RequiresApi(Build.VERSION_CODES.N) + fun onKeyEvent(data: ByteArray) { + val keyEvent = KeyEvent.parseFrom(data) + val keyboardMode = keyEvent.getMode() + + var textToCommit: String? = null + + // [down] indicates the key's state(down or up). + // [press] indicates a click event(down and up). + // https://github.com/rustdesk/rustdesk/blob/3a7594755341f023f56fa4b6a43b60d6b47df88d/flutter/lib/models/input_model.dart#L688 + if (keyEvent.hasSeq()) { + textToCommit = keyEvent.getSeq() + } else if (keyboardMode == KeyboardMode.Legacy) { + if (keyEvent.hasChr() && (keyEvent.getDown() || keyEvent.getPress())) { + val chr = keyEvent.getChr() + if (chr != null) { + textToCommit = String(Character.toChars(chr)) + } + } + } else if (keyboardMode == KeyboardMode.Translate) { + } else { + } + + Log.d(logTag, "onKeyEvent $keyEvent textToCommit:$textToCommit") + + var ke: KeyEventAndroid? = null + if (Build.VERSION.SDK_INT < 33 || textToCommit == null) { + ke = KeyEventConverter.toAndroidKeyEvent(keyEvent) + } + ke?.let { event -> + if (tryHandleVolumeKeyEvent(event)) { + return + } else if (tryHandlePowerKeyEvent(event)) { + return + } + } + + if (Build.VERSION.SDK_INT >= 33) { + getInputMethod()?.let { inputMethod -> + inputMethod.getCurrentInputConnection()?.let { inputConnection -> + if (textToCommit != null) { + textToCommit?.let { text -> + inputConnection.commitText(text, 1, null) + } + } else { + ke?.let { event -> + inputConnection.sendKeyEvent(event) + if (keyEvent.getPress()) { + val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode) + inputConnection.sendKeyEvent(actionUpEvent) + } + } + } + } + } + } else { + val handler = Handler(Looper.getMainLooper()) + handler.post { + ke?.let { event -> + val possibleNodes = possibleAccessibiltyNodes() + Log.d(logTag, "possibleNodes:$possibleNodes") + for (item in possibleNodes) { + val success = trySendKeyEvent(event, item, textToCommit) + if (success) { + if (keyEvent.getPress()) { + val actionUpEvent = KeyEventAndroid(KeyEventAndroid.ACTION_UP, event.keyCode) + trySendKeyEvent(actionUpEvent, item, textToCommit) + } + break + } + } + } + } + } + } + + private fun tryHandleVolumeKeyEvent(event: KeyEventAndroid): Boolean { + when (event.keyCode) { + KeyEventAndroid.KEYCODE_VOLUME_UP -> { + if (event.action == KeyEventAndroid.ACTION_DOWN) { + volumeController.raiseVolume(null, true, AudioManager.STREAM_SYSTEM) + } + return true + } + KeyEventAndroid.KEYCODE_VOLUME_DOWN -> { + if (event.action == KeyEventAndroid.ACTION_DOWN) { + volumeController.lowerVolume(null, true, AudioManager.STREAM_SYSTEM) + } + return true + } + KeyEventAndroid.KEYCODE_VOLUME_MUTE -> { + if (event.action == KeyEventAndroid.ACTION_DOWN) { + volumeController.toggleMute(true, AudioManager.STREAM_SYSTEM) + } + return true + } + else -> { + return false + } + } + } + + private fun tryHandlePowerKeyEvent(event: KeyEventAndroid): Boolean { + if (event.keyCode == KeyEventAndroid.KEYCODE_POWER) { + // Perform power dialog action when action is up + if (event.action == KeyEventAndroid.ACTION_UP) { + performGlobalAction(GLOBAL_ACTION_POWER_DIALOG); + } + return true + } + return false + } + + private fun insertAccessibilityNode(list: LinkedList, node: AccessibilityNodeInfo) { + if (node == null) { + return + } + if (list.contains(node)) { + return + } + list.add(node) + } + + private fun findChildNode(node: AccessibilityNodeInfo?): AccessibilityNodeInfo? { + if (node == null) { + return null + } + if (node.isEditable() && node.isFocusable()) { + return node + } + val childCount = node.getChildCount() + for (i in 0 until childCount) { + val child = node.getChild(i) + if (child != null) { + if (child.isEditable() && child.isFocusable()) { + return child + } + if (Build.VERSION.SDK_INT < 33) { + child.recycle() + } + } + } + for (i in 0 until childCount) { + val child = node.getChild(i) + if (child != null) { + val result = findChildNode(child) + if (Build.VERSION.SDK_INT < 33) { + if (child != result) { + child.recycle() + } + } + if (result != null) { + return result + } + } + } + return null + } + + private fun possibleAccessibiltyNodes(): LinkedList { + val linkedList = LinkedList() + val latestList = LinkedList() + + val focusInput = findFocus(AccessibilityNodeInfo.FOCUS_INPUT) + var focusAccessibilityInput = findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY) + + val rootInActiveWindow = getRootInActiveWindow() + + Log.d(logTag, "focusInput:$focusInput focusAccessibilityInput:$focusAccessibilityInput rootInActiveWindow:$rootInActiveWindow") + + if (focusInput != null) { + if (focusInput.isFocusable() && focusInput.isEditable()) { + insertAccessibilityNode(linkedList, focusInput) + } else { + insertAccessibilityNode(latestList, focusInput) + } + } + + if (focusAccessibilityInput != null) { + if (focusAccessibilityInput.isFocusable() && focusAccessibilityInput.isEditable()) { + insertAccessibilityNode(linkedList, focusAccessibilityInput) + } else { + insertAccessibilityNode(latestList, focusAccessibilityInput) + } + } + + val childFromFocusInput = findChildNode(focusInput) + Log.d(logTag, "childFromFocusInput:$childFromFocusInput") + + if (childFromFocusInput != null) { + insertAccessibilityNode(linkedList, childFromFocusInput) + } + + val childFromFocusAccessibilityInput = findChildNode(focusAccessibilityInput) + if (childFromFocusAccessibilityInput != null) { + insertAccessibilityNode(linkedList, childFromFocusAccessibilityInput) + } + Log.d(logTag, "childFromFocusAccessibilityInput:$childFromFocusAccessibilityInput") + + if (rootInActiveWindow != null) { + insertAccessibilityNode(linkedList, rootInActiveWindow) + } + + for (item in latestList) { + insertAccessibilityNode(linkedList, item) + } + + return linkedList + } + + private fun trySendKeyEvent(event: KeyEventAndroid, node: AccessibilityNodeInfo, textToCommit: String?): Boolean { + node.refresh() + this.fakeEditTextForTextStateCalculation?.setSelection(0,0) + this.fakeEditTextForTextStateCalculation?.setText(null) + + val text = node.getText() + var isShowingHint = false + if (Build.VERSION.SDK_INT >= 26) { + isShowingHint = node.isShowingHintText() + } + + var textSelectionStart = node.textSelectionStart + var textSelectionEnd = node.textSelectionEnd + + if (text != null) { + if (textSelectionStart > text.length) { + textSelectionStart = text.length + } + if (textSelectionEnd > text.length) { + textSelectionEnd = text.length + } + if (textSelectionStart > textSelectionEnd) { + textSelectionStart = textSelectionEnd + } + } + + var success = false + + Log.d(logTag, "existing text:$text textToCommit:$textToCommit textSelectionStart:$textSelectionStart textSelectionEnd:$textSelectionEnd") + + if (textToCommit != null) { + if ((textSelectionStart == -1) || (textSelectionEnd == -1)) { + val newText = textToCommit + this.fakeEditTextForTextStateCalculation?.setText(newText) + success = updateTextForAccessibilityNode(node) + } else if (text != null) { + this.fakeEditTextForTextStateCalculation?.setText(text) + this.fakeEditTextForTextStateCalculation?.setSelection( + textSelectionStart, + textSelectionEnd + ) + this.fakeEditTextForTextStateCalculation?.text?.insert(textSelectionStart, textToCommit) + success = updateTextAndSelectionForAccessibiltyNode(node) + } + } else { + if (isShowingHint) { + this.fakeEditTextForTextStateCalculation?.setText(null) + } else { + this.fakeEditTextForTextStateCalculation?.setText(text) + } + if (textSelectionStart != -1 && textSelectionEnd != -1) { + Log.d(logTag, "setting selection $textSelectionStart $textSelectionEnd") + this.fakeEditTextForTextStateCalculation?.setSelection( + textSelectionStart, + textSelectionEnd + ) + } + + this.fakeEditTextForTextStateCalculation?.let { + // This is essiential to make sure layout object is created. OnKeyDown may not work if layout is not created. + val rect = Rect() + node.getBoundsInScreen(rect) + + it.layout(rect.left, rect.top, rect.right, rect.bottom) + it.onPreDraw() + if (event.action == KeyEventAndroid.ACTION_DOWN) { + val succ = it.onKeyDown(event.getKeyCode(), event) + Log.d(logTag, "onKeyDown $succ") + } else if (event.action == KeyEventAndroid.ACTION_UP) { + val success = it.onKeyUp(event.getKeyCode(), event) + Log.d(logTag, "keyup $success") + } else {} + } + + success = updateTextAndSelectionForAccessibiltyNode(node) + } + return success + } + + fun updateTextForAccessibilityNode(node: AccessibilityNodeInfo): Boolean { + var success = false + this.fakeEditTextForTextStateCalculation?.text?.let { + val arguments = Bundle() + arguments.putCharSequence( + AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, + it.toString() + ) + success = node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments) + } + return success + } + + fun updateTextAndSelectionForAccessibiltyNode(node: AccessibilityNodeInfo): Boolean { + var success = updateTextForAccessibilityNode(node) + + if (success) { + val selectionStart = this.fakeEditTextForTextStateCalculation?.selectionStart + val selectionEnd = this.fakeEditTextForTextStateCalculation?.selectionEnd + + if (selectionStart != null && selectionEnd != null) { + val arguments = Bundle() + arguments.putInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, + selectionStart + ) + arguments.putInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, + selectionEnd + ) + success = node.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, arguments) + Log.d(logTag, "Update selection to $selectionStart $selectionEnd success:$success") + } + } + + return success + } + + + override fun onAccessibilityEvent(event: AccessibilityEvent) { + } + + override fun onServiceConnected() { + super.onServiceConnected() + ctx = this + val info = AccessibilityServiceInfo() + if (Build.VERSION.SDK_INT >= 33) { + info.flags = FLAG_INPUT_METHOD_EDITOR or FLAG_RETRIEVE_INTERACTIVE_WINDOWS + } else { + info.flags = FLAG_RETRIEVE_INTERACTIVE_WINDOWS + } + setServiceInfo(info) + fakeEditTextForTextStateCalculation = EditText(this) + // Size here doesn't matter, we won't show this view. + fakeEditTextForTextStateCalculation?.layoutParams = LayoutParams(100, 100) + fakeEditTextForTextStateCalculation?.onPreDraw() + val layout = fakeEditTextForTextStateCalculation?.getLayout() + Log.d(logTag, "fakeEditTextForTextStateCalculation layout:$layout") + Log.d(logTag, "onServiceConnected!") + } + + override fun onDestroy() { + ctx = null + super.onDestroy() + } + + override fun onInterrupt() {} +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt new file mode 100644 index 0000000..ccb3319 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/KeyboardKeyEventMapper.kt @@ -0,0 +1,122 @@ +package hbb; +import android.view.KeyEvent +import android.view.KeyCharacterMap +import hbb.MessageOuterClass.KeyboardMode +import hbb.MessageOuterClass.ControlKey + +object KeyEventConverter { + fun toAndroidKeyEvent(keyEventProto: hbb.MessageOuterClass.KeyEvent): KeyEvent { + var chrValue = 0 + var modifiers = 0 + + val keyboardMode = keyEventProto.getMode() + + if (keyEventProto.hasChr()) { + if (keyboardMode == KeyboardMode.Map || keyboardMode == KeyboardMode.Translate) { + chrValue = keyEventProto.getChr() + } else { + chrValue = convertUnicodeToKeyCode(keyEventProto.getChr() as Int) + } + } else if (keyEventProto.hasControlKey()) { + chrValue = convertControlKeyToKeyCode(keyEventProto.getControlKey()) + } + + var modifiersList = keyEventProto.getModifiersList() + + if (modifiersList != null) { + for (modifier in keyEventProto.getModifiersList()) { + val modifierValue = convertModifier(modifier) + modifiers = modifiers or modifierValue + } + } + + var action = 0 + if (keyEventProto.getDown() || keyEventProto.getPress()) { + action = KeyEvent.ACTION_DOWN + } else { + action = KeyEvent.ACTION_UP + } + + return KeyEvent(0, 0, action, chrValue, 0, modifiers) + } + + private fun convertModifier(controlKey: hbb.MessageOuterClass.ControlKey): Int { + // Add logic to map ControlKey values to Android KeyEvent key codes. + // You'll need to provide the mapping for each key. + return when (controlKey) { + ControlKey.Alt -> KeyEvent.META_ALT_ON + ControlKey.Control -> KeyEvent.META_CTRL_ON + ControlKey.CapsLock -> KeyEvent.META_CAPS_LOCK_ON + ControlKey.Meta -> KeyEvent.META_META_ON + ControlKey.NumLock -> KeyEvent.META_NUM_LOCK_ON + ControlKey.RShift -> KeyEvent.META_SHIFT_RIGHT_ON + ControlKey.Shift -> KeyEvent.META_SHIFT_ON + ControlKey.RAlt -> KeyEvent.META_ALT_RIGHT_ON + ControlKey.RControl -> KeyEvent.META_CTRL_RIGHT_ON + else -> 0 // Default to unknown. + } + } + + private val tag = "KeyEventConverter" + + private fun convertUnicodeToKeyCode(unicode: Int): Int { + val charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD) + android.util.Log.d(tag, "unicode: $unicode") + val events = charMap.getEvents(charArrayOf(unicode.toChar())) + if (events != null && events.size > 0) { + android.util.Log.d(tag, "keycode ${events[0].keyCode}") + return events[0].keyCode + } + return 0 + } + + private fun convertControlKeyToKeyCode(controlKey: hbb.MessageOuterClass.ControlKey): Int { + // Add logic to map ControlKey values to Android KeyEvent key codes. + // You'll need to provide the mapping for each key. + return when (controlKey) { + ControlKey.Alt -> KeyEvent.KEYCODE_ALT_LEFT + ControlKey.Backspace -> KeyEvent.KEYCODE_DEL + ControlKey.Control -> KeyEvent.KEYCODE_CTRL_LEFT + ControlKey.CapsLock -> KeyEvent.KEYCODE_CAPS_LOCK + ControlKey.Meta -> KeyEvent.KEYCODE_META_LEFT + ControlKey.NumLock -> KeyEvent.KEYCODE_NUM_LOCK + ControlKey.RShift -> KeyEvent.KEYCODE_SHIFT_RIGHT + ControlKey.Shift -> KeyEvent.KEYCODE_SHIFT_LEFT + ControlKey.RAlt -> KeyEvent.KEYCODE_ALT_RIGHT + ControlKey.RControl -> KeyEvent.KEYCODE_CTRL_RIGHT + ControlKey.DownArrow -> KeyEvent.KEYCODE_DPAD_DOWN + ControlKey.LeftArrow -> KeyEvent.KEYCODE_DPAD_LEFT + ControlKey.RightArrow -> KeyEvent.KEYCODE_DPAD_RIGHT + ControlKey.UpArrow -> KeyEvent.KEYCODE_DPAD_UP + ControlKey.End -> KeyEvent.KEYCODE_MOVE_END + ControlKey.Home -> KeyEvent.KEYCODE_MOVE_HOME + ControlKey.PageUp -> KeyEvent.KEYCODE_PAGE_UP + ControlKey.PageDown -> KeyEvent.KEYCODE_PAGE_DOWN + ControlKey.Insert -> KeyEvent.KEYCODE_INSERT + ControlKey.Escape -> KeyEvent.KEYCODE_ESCAPE + ControlKey.F1 -> KeyEvent.KEYCODE_F1 + ControlKey.F2 -> KeyEvent.KEYCODE_F2 + ControlKey.F3 -> KeyEvent.KEYCODE_F3 + ControlKey.F4 -> KeyEvent.KEYCODE_F4 + ControlKey.F5 -> KeyEvent.KEYCODE_F5 + ControlKey.F6 -> KeyEvent.KEYCODE_F6 + ControlKey.F7 -> KeyEvent.KEYCODE_F7 + ControlKey.F8 -> KeyEvent.KEYCODE_F8 + ControlKey.F9 -> KeyEvent.KEYCODE_F9 + ControlKey.F10 -> KeyEvent.KEYCODE_F10 + ControlKey.F11 -> KeyEvent.KEYCODE_F11 + ControlKey.F12 -> KeyEvent.KEYCODE_F12 + ControlKey.Space -> KeyEvent.KEYCODE_SPACE + ControlKey.Tab -> KeyEvent.KEYCODE_TAB + ControlKey.Return -> KeyEvent.KEYCODE_ENTER + ControlKey.Delete -> KeyEvent.KEYCODE_FORWARD_DEL + ControlKey.Clear -> KeyEvent.KEYCODE_CLEAR + ControlKey.Pause -> KeyEvent.KEYCODE_BREAK + ControlKey.VolumeMute -> KeyEvent.KEYCODE_VOLUME_MUTE + ControlKey.VolumeUp -> KeyEvent.KEYCODE_VOLUME_UP + ControlKey.VolumeDown -> KeyEvent.KEYCODE_VOLUME_DOWN + ControlKey.Power -> KeyEvent.KEYCODE_POWER + else -> 0 // Default to unknown. + } + } +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt new file mode 100644 index 0000000..fea8e55 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -0,0 +1,414 @@ +package com.carriez.flutter_hbb + +/** + * Handle events from flutter + * Request MediaProjection permission + * + * Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG + */ + +import ffi.FFI + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.ClipboardManager +import android.os.Bundle +import android.os.Build +import android.os.IBinder +import android.util.Log +import android.view.WindowManager +import android.media.MediaCodecInfo +import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface +import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar +import android.media.MediaCodecList +import android.media.MediaFormat +import android.util.DisplayMetrics +import androidx.annotation.RequiresApi +import org.json.JSONArray +import org.json.JSONObject +import com.hjq.permissions.XXPermissions +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel +import kotlin.concurrent.thread + + +class MainActivity : FlutterActivity() { + companion object { + var flutterMethodChannel: MethodChannel? = null + private var _rdClipboardManager: RdClipboardManager? = null + val rdClipboardManager: RdClipboardManager? + get() = _rdClipboardManager; + } + + private val channelTag = "mChannel" + private val logTag = "mMainActivity" + private var mainService: MainService? = null + + private var isAudioStart = false + private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart }) + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + if (MainService.isReady) { + Intent(activity, MainService::class.java).also { + bindService(it, serviceConnection, Context.BIND_AUTO_CREATE) + } + } + flutterMethodChannel = MethodChannel( + flutterEngine.dartExecutor.binaryMessenger, + channelTag + ) + initFlutterChannel(flutterMethodChannel!!) + thread { + try { + setCodecInfo() + } catch (e: Exception) { + Log.e("MainActivity", "Failed to setCodecInfo: ${e.message}", e) + } + } + } + + override fun onResume() { + super.onResume() + val inputPer = InputService.isOpen + activity.runOnUiThread { + flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "input", "value" to inputPer.toString()) + ) + } + } + + private fun requestMediaProjection() { + val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply { + action = ACT_REQUEST_MEDIA_PROJECTION + } + startActivityForResult(intent, REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) { + flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (_rdClipboardManager == null) { + _rdClipboardManager = RdClipboardManager(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager) + FFI.setClipboardManager(_rdClipboardManager!!) + } + } + + override fun onDestroy() { + Log.e(logTag, "onDestroy") + mainService?.let { + unbindService(serviceConnection) + } + super.onDestroy() + } + + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + Log.d(logTag, "onServiceConnected") + val binder = service as MainService.LocalBinder + mainService = binder.getService() + } + + override fun onServiceDisconnected(name: ComponentName?) { + Log.d(logTag, "onServiceDisconnected") + mainService = null + } + } + + private fun initFlutterChannel(flutterMethodChannel: MethodChannel) { + flutterMethodChannel.setMethodCallHandler { call, result -> + // make sure result will be invoked, otherwise flutter will await forever + when (call.method) { + "init_service" -> { + Intent(activity, MainService::class.java).also { + bindService(it, serviceConnection, Context.BIND_AUTO_CREATE) + } + if (MainService.isReady) { + result.success(false) + return@setMethodCallHandler + } + requestMediaProjection() + result.success(true) + } + "start_capture" -> { + mainService?.let { + result.success(it.startCapture()) + } ?: let { + result.success(false) + } + } + "stop_service" -> { + Log.d(logTag, "Stop service") + mainService?.let { + it.destroy() + result.success(true) + } ?: let { + result.success(false) + } + } + "check_permission" -> { + if (call.arguments is String) { + result.success(XXPermissions.isGranted(context, call.arguments as String)) + } else { + result.success(false) + } + } + "request_permission" -> { + if (call.arguments is String) { + requestPermission(context, call.arguments as String) + result.success(true) + } else { + result.success(false) + } + } + START_ACTION -> { + if (call.arguments is String) { + startAction(context, call.arguments as String) + result.success(true) + } else { + result.success(false) + } + } + "check_video_permission" -> { + mainService?.let { + result.success(it.checkMediaPermission()) + } ?: let { + result.success(false) + } + } + "check_service" -> { + Companion.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "input", "value" to InputService.isOpen.toString()) + ) + Companion.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "media", "value" to MainService.isReady.toString()) + ) + result.success(true) + } + "stop_input" -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + InputService.ctx?.disableSelf() + } + InputService.ctx = null + Companion.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "input", "value" to InputService.isOpen.toString()) + ) + result.success(true) + } + "cancel_notification" -> { + if (call.arguments is Int) { + val id = call.arguments as Int + mainService?.cancelNotification(id) + } else { + result.success(true) + } + } + "enable_soft_keyboard" -> { + // https://blog.csdn.net/hanye2020/article/details/105553780 + if (call.arguments as Boolean) { + window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + } + result.success(true) + + } + "try_sync_clipboard" -> { + rdClipboardManager?.syncClipboard(true) + result.success(true) + } + GET_START_ON_BOOT_OPT -> { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) + } + SET_START_ON_BOOT_OPT -> { + if (call.arguments is Boolean) { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + val edit = prefs.edit() + edit.putBoolean(KEY_START_ON_BOOT_OPT, call.arguments as Boolean) + edit.apply() + result.success(true) + } else { + result.success(false) + } + } + SYNC_APP_DIR_CONFIG_PATH -> { + if (call.arguments is String) { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + val edit = prefs.edit() + edit.putString(KEY_APP_DIR_CONFIG_PATH, call.arguments as String) + edit.apply() + result.success(true) + } else { + result.success(false) + } + } + GET_VALUE -> { + if (call.arguments is String) { + if (call.arguments == KEY_IS_SUPPORT_VOICE_CALL) { + result.success(isSupportVoiceCall()) + } else { + result.error("-1", "No such key", null) + } + } else { + result.success(null) + } + } + "on_voice_call_started" -> { + onVoiceCallStarted() + } + "on_voice_call_closed" -> { + onVoiceCallClosed() + } + else -> { + result.error("-1", "No such method", null) + } + } + } + } + + private fun setCodecInfo() { + val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS) + val codecs = codecList.codecInfos + val codecArray = JSONArray() + + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + val wh = getScreenSize(windowManager) + var w = wh.first + var h = wh.second + val align = 64 + w = (w + align - 1) / align * align + h = (h + align - 1) / align * align + codecs.forEach { codec -> + val codecObject = JSONObject() + codecObject.put("name", codec.name) + codecObject.put("is_encoder", codec.isEncoder) + var hw: Boolean? = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + hw = codec.isHardwareAccelerated + } else { + // https://chromium.googlesource.com/external/webrtc/+/HEAD/sdk/android/src/java/org/webrtc/MediaCodecUtils.java#29 + // https://chromium.googlesource.com/external/webrtc/+/master/sdk/android/api/org/webrtc/HardwareVideoEncoderFactory.java#229 + if (listOf("OMX.google.", "OMX.SEC.", "c2.android").any { codec.name.startsWith(it, true) }) { + hw = false + } else if (listOf("c2.qti", "OMX.qcom.video", "OMX.Exynos", "OMX.hisi", "OMX.MTK", "OMX.Intel", "OMX.Nvidia").any { codec.name.startsWith(it, true) }) { + hw = true + } + } + if (hw != true) { + return@forEach + } + codecObject.put("hw", hw) + var mime_type = "" + codec.supportedTypes.forEach { type -> + if (listOf("video/avc", "video/hevc").contains(type)) { // "video/x-vnd.on2.vp8", "video/x-vnd.on2.vp9", "video/av01" + mime_type = type; + } + } + if (mime_type.isNotEmpty()) { + codecObject.put("mime_type", mime_type) + val caps = codec.getCapabilitiesForType(mime_type) + if (codec.isEncoder) { + // Encoder's max_height and max_width are interchangeable + if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) { + return@forEach + } + } + codecObject.put("min_width", caps.videoCapabilities.supportedWidths.lower) + codecObject.put("max_width", caps.videoCapabilities.supportedWidths.upper) + codecObject.put("min_height", caps.videoCapabilities.supportedHeights.lower) + codecObject.put("max_height", caps.videoCapabilities.supportedHeights.upper) + val surface = caps.colorFormats.contains(COLOR_FormatSurface); + codecObject.put("surface", surface) + val nv12 = caps.colorFormats.contains(COLOR_FormatYUV420SemiPlanar) + codecObject.put("nv12", nv12) + if (!(nv12 || surface)) { + return@forEach + } + codecObject.put("min_bitrate", caps.videoCapabilities.bitrateRange.lower / 1000) + codecObject.put("max_bitrate", caps.videoCapabilities.bitrateRange.upper / 1000) + if (!codec.isEncoder) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + codecObject.put("low_latency", caps.isFeatureSupported(MediaCodecInfo.CodecCapabilities.FEATURE_LowLatency)) + } + } + if (!codec.isEncoder) { + return@forEach + } + codecArray.put(codecObject) + } + } + val result = JSONObject() + result.put("version", Build.VERSION.SDK_INT) + result.put("w", w) + result.put("h", h) + result.put("codecs", codecArray) + FFI.setCodecInfo(result.toString()) + } + + private fun onVoiceCallStarted() { + var ok = false + mainService?.let { + ok = it.onVoiceCallStarted() + } ?: let { + isAudioStart = true + ok = audioRecordHandle.onVoiceCallStarted(null) + } + if (!ok) { + // Rarely happens, So we just add log and msgbox here. + Log.e(logTag, "onVoiceCallStarted fail") + flutterMethodChannel?.invokeMethod("msgbox", mapOf( + "type" to "custom-nook-nocancel-hasclose-error", + "title" to "Voice call", + "text" to "Failed to start voice call.")) + } else { + Log.d(logTag, "onVoiceCallStarted success") + } + } + + private fun onVoiceCallClosed() { + var ok = false + mainService?.let { + ok = it.onVoiceCallClosed() + } ?: let { + isAudioStart = false + ok = audioRecordHandle.onVoiceCallClosed(null) + } + if (!ok) { + // Rarely happens, So we just add log and msgbox here. + Log.e(logTag, "onVoiceCallClosed fail") + flutterMethodChannel?.invokeMethod("msgbox", mapOf( + "type" to "custom-nook-nocancel-hasclose-error", + "title" to "Voice call", + "text" to "Failed to stop voice call.")) + } else { + Log.d(logTag, "onVoiceCallClosed success") + } + } + + override fun onStop() { + super.onStop() + val disableFloatingWindow = FFI.getLocalOption("disable-floating-window") == "Y" + if (!disableFloatingWindow && MainService.isReady) { + startService(Intent(this, FloatingWindowService::class.java)) + } + } + + override fun onStart() { + super.onStart() + stopService(Intent(this, FloatingWindowService::class.java)) + } +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainApplication.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainApplication.kt new file mode 100644 index 0000000..59a3b0f --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainApplication.kt @@ -0,0 +1,17 @@ +package com.carriez.flutter_hbb + +import android.app.Application +import android.util.Log +import ffi.FFI + +class MainApplication : Application() { + companion object { + private const val TAG = "MainApplication" + } + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "App start") + FFI.onAppStart(applicationContext) + } +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt new file mode 100644 index 0000000..7bb16a0 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -0,0 +1,729 @@ +package com.carriez.flutter_hbb + +import ffi.FFI + +/** + * Capture screen,get video and audio,send to rust. + * Dispatch notifications + * + * Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG + */ + +import android.Manifest +import android.annotation.SuppressLint +import android.app.* +import android.app.PendingIntent.FLAG_IMMUTABLE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.graphics.Color +import android.graphics.PixelFormat +import android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR +import android.hardware.display.VirtualDisplay +import android.media.* +import android.media.projection.MediaProjection +import android.media.projection.MediaProjectionManager +import android.os.* +import android.util.DisplayMetrics +import android.util.Log +import android.view.Surface +import android.view.Surface.FRAME_RATE_COMPATIBILITY_DEFAULT +import android.view.WindowManager +import androidx.annotation.Keep +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import io.flutter.embedding.android.FlutterActivity +import java.util.concurrent.Executors +import kotlin.concurrent.thread +import org.json.JSONException +import org.json.JSONObject +import java.nio.ByteBuffer +import kotlin.math.max +import kotlin.math.min + +const val DEFAULT_NOTIFY_TITLE = "RustDesk" +const val DEFAULT_NOTIFY_TEXT = "Service is running" +const val DEFAULT_NOTIFY_ID = 1 +const val NOTIFY_ID_OFFSET = 100 + +const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9 + +// video const + +const val MAX_SCREEN_SIZE = 1200 + +const val VIDEO_KEY_BIT_RATE = 1024_000 +const val VIDEO_KEY_FRAME_RATE = 30 + +class MainService : Service() { + + @Keep + @RequiresApi(Build.VERSION_CODES.N) + fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) { + // turn on screen with LEFT_DOWN when screen off + if (!powerManager.isInteractive && (kind == 0 || mask == LEFT_DOWN)) { + if (wakeLock.isHeld) { + Log.d(logTag, "Turn on Screen, WakeLock release") + wakeLock.release() + } + Log.d(logTag,"Turn on Screen") + wakeLock.acquire(5000) + } else { + when (kind) { + 0 -> { // touch + InputService.ctx?.onTouchInput(mask, x, y) + } + 1 -> { // mouse + InputService.ctx?.onMouseInput(mask, x, y) + } + else -> { + } + } + } + } + + @Keep + @RequiresApi(Build.VERSION_CODES.N) + fun rustKeyEventInput(input: ByteArray) { + InputService.ctx?.onKeyEvent(input) + } + + @Keep + fun rustGetByName(name: String): String { + return when (name) { + "screen_size" -> { + JSONObject().apply { + put("width",SCREEN_INFO.width) + put("height",SCREEN_INFO.height) + put("scale",SCREEN_INFO.scale) + }.toString() + } + "is_start" -> { + isStart.toString() + } + else -> "" + } + } + + @Keep + fun rustSetByName(name: String, arg1: String, arg2: String) { + when (name) { + "add_connection" -> { + try { + val jsonObject = JSONObject(arg1) + val id = jsonObject["id"] as Int + val username = jsonObject["name"] as String + val peerId = jsonObject["peer_id"] as String + val authorized = jsonObject["authorized"] as Boolean + val isFileTransfer = jsonObject["is_file_transfer"] as Boolean + val type = if (isFileTransfer) { + translate("Transfer file") + } else { + translate("Share screen") + } + if (authorized) { + if (!isFileTransfer && !isStart) { + startCapture() + } + onClientAuthorizedNotification(id, type, username, peerId) + } else { + loginRequestNotification(id, type, username, peerId) + } + } catch (e: JSONException) { + e.printStackTrace() + } + } + "update_voice_call_state" -> { + try { + val jsonObject = JSONObject(arg1) + val id = jsonObject["id"] as Int + val username = jsonObject["name"] as String + val peerId = jsonObject["peer_id"] as String + val inVoiceCall = jsonObject["in_voice_call"] as Boolean + val incomingVoiceCall = jsonObject["incoming_voice_call"] as Boolean + if (!inVoiceCall) { + if (incomingVoiceCall) { + voiceCallRequestNotification(id, "Voice Call Request", username, peerId) + } else { + if (!audioRecordHandle.switchOutVoiceCall(mediaProjection)) { + Log.e(logTag, "switchOutVoiceCall fail") + MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf( + "type" to "custom-nook-nocancel-hasclose-error", + "title" to "Voice call", + "text" to "Failed to switch out voice call.")) + } + } + } else { + if (!audioRecordHandle.switchToVoiceCall(mediaProjection)) { + Log.e(logTag, "switchToVoiceCall fail") + MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf( + "type" to "custom-nook-nocancel-hasclose-error", + "title" to "Voice call", + "text" to "Failed to switch to voice call.")) + } + } + } catch (e: JSONException) { + e.printStackTrace() + } + } + "stop_capture" -> { + Log.d(logTag, "from rust:stop_capture") + stopCapture() + } + "half_scale" -> { + val halfScale = arg1.toBoolean() + if (isHalfScale != halfScale) { + isHalfScale = halfScale + updateScreenInfo(resources.configuration.orientation) + } + + } + else -> { + } + } + } + + private var serviceLooper: Looper? = null + private var serviceHandler: Handler? = null + + private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager } + private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")} + + companion object { + private var _isReady = false // media permission ready status + private var _isStart = false // screen capture start status + private var _isAudioStart = false // audio capture start status + val isReady: Boolean + get() = _isReady + val isStart: Boolean + get() = _isStart + val isAudioStart: Boolean + get() = _isAudioStart + } + + private val logTag = "LOG_SERVICE" + private val useVP9 = false + private val binder = LocalBinder() + + private var reuseVirtualDisplay = Build.VERSION.SDK_INT > 33 + + // video + private var mediaProjection: MediaProjection? = null + private var surface: Surface? = null + private val sendVP9Thread = Executors.newSingleThreadExecutor() + private var videoEncoder: MediaCodec? = null + private var imageReader: ImageReader? = null + private var virtualDisplay: VirtualDisplay? = null + + // audio + private val audioRecordHandle = AudioRecordHandle(this, { isStart }, { isAudioStart }) + + // notification + private lateinit var notificationManager: NotificationManager + private lateinit var notificationChannel: String + private lateinit var notificationBuilder: NotificationCompat.Builder + + override fun onCreate() { + super.onCreate() + Log.d(logTag,"MainService onCreate, sdk int:${Build.VERSION.SDK_INT} reuseVirtualDisplay:$reuseVirtualDisplay") + FFI.init(this) + HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply { + start() + serviceLooper = looper + serviceHandler = Handler(looper) + } + updateScreenInfo(resources.configuration.orientation) + initNotification() + + // keep the config dir same with flutter + val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE) + val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: "" + FFI.startServer(configPath, "") + + createForegroundNotification() + } + + override fun onDestroy() { + checkMediaPermission() + stopService(Intent(this, FloatingWindowService::class.java)) + super.onDestroy() + } + + private var isHalfScale: Boolean? = null; + private fun updateScreenInfo(orientation: Int) { + var w: Int + var h: Int + var dpi: Int + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + + @Suppress("DEPRECATION") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val m = windowManager.maximumWindowMetrics + w = m.bounds.width() + h = m.bounds.height() + dpi = resources.configuration.densityDpi + } else { + val dm = DisplayMetrics() + windowManager.defaultDisplay.getRealMetrics(dm) + w = dm.widthPixels + h = dm.heightPixels + dpi = dm.densityDpi + } + + val max = max(w,h) + val min = min(w,h) + if (orientation == ORIENTATION_LANDSCAPE) { + w = max + h = min + } else { + w = min + h = max + } + Log.d(logTag,"updateScreenInfo:w:$w,h:$h") + var scale = 1 + if (w != 0 && h != 0) { + if (isHalfScale == true && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) { + scale = 2 + w /= scale + h /= scale + dpi /= scale + } + if (SCREEN_INFO.width != w) { + SCREEN_INFO.width = w + SCREEN_INFO.height = h + SCREEN_INFO.scale = scale + SCREEN_INFO.dpi = dpi + if (isStart) { + stopCapture() + FFI.refreshScreen() + startCapture() + } else { + FFI.refreshScreen() + } + } + + } + } + + override fun onBind(intent: Intent): IBinder { + Log.d(logTag, "service onBind") + return binder + } + + inner class LocalBinder : Binder() { + init { + Log.d(logTag, "LocalBinder init") + } + + fun getService(): MainService = this@MainService + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.d("whichService", "this service: ${Thread.currentThread()}") + super.onStartCommand(intent, flags, startId) + if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) { + createForegroundNotification() + + if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) { + FFI.startService() + } + Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}") + val mediaProjectionManager = + getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + + intent.getParcelableExtra(EXT_MEDIA_PROJECTION_RES_INTENT)?.let { + mediaProjection = + mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it) + checkMediaPermission() + _isReady = true + } ?: let { + Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection") + requestMediaProjection() + } + } + return START_NOT_STICKY // don't use sticky (auto restart), the new service (from auto restart) will lose control + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + updateScreenInfo(newConfig.orientation) + } + + private fun requestMediaProjection() { + val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply { + action = ACT_REQUEST_MEDIA_PROJECTION + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + } + + @SuppressLint("WrongConstant") + private fun createSurface(): Surface? { + return if (useVP9) { + // TODO + null + } else { + Log.d(logTag, "ImageReader.newInstance:INFO:$SCREEN_INFO") + imageReader = + ImageReader.newInstance( + SCREEN_INFO.width, + SCREEN_INFO.height, + PixelFormat.RGBA_8888, + 4 + ).apply { + setOnImageAvailableListener({ imageReader: ImageReader -> + try { + // If not call acquireLatestImage, listener will not be called again + imageReader.acquireLatestImage().use { image -> + if (image == null || !isStart) return@setOnImageAvailableListener + val planes = image.planes + val buffer = planes[0].buffer + buffer.rewind() + FFI.onVideoFrameUpdate(buffer) + } + } catch (ignored: java.lang.Exception) { + } + }, serviceHandler) + } + Log.d(logTag, "ImageReader.setOnImageAvailableListener done") + imageReader?.surface + } + } + + fun onVoiceCallStarted(): Boolean { + return audioRecordHandle.onVoiceCallStarted(mediaProjection) + } + + fun onVoiceCallClosed(): Boolean { + return audioRecordHandle.onVoiceCallClosed(mediaProjection) + } + + fun startCapture(): Boolean { + if (isStart) { + return true + } + if (mediaProjection == null) { + Log.w(logTag, "startCapture fail,mediaProjection is null") + return false + } + + updateScreenInfo(resources.configuration.orientation) + Log.d(logTag, "Start Capture") + surface = createSurface() + + if (useVP9) { + startVP9VideoRecorder(mediaProjection!!) + } else { + startRawVideoRecorder(mediaProjection!!) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (!audioRecordHandle.createAudioRecorder(false, mediaProjection)) { + Log.d(logTag, "createAudioRecorder fail") + } else { + Log.d(logTag, "audio recorder start") + audioRecordHandle.startAudioRecorder() + } + } + checkMediaPermission() + _isStart = true + FFI.setFrameRawEnable("video",true) + MainActivity.rdClipboardManager?.setCaptureStarted(_isStart) + return true + } + + @Synchronized + fun stopCapture() { + Log.d(logTag, "Stop Capture") + FFI.setFrameRawEnable("video",false) + _isStart = false + MainActivity.rdClipboardManager?.setCaptureStarted(_isStart) + // release video + if (reuseVirtualDisplay) { + // The virtual display video projection can be paused by calling `setSurface(null)`. + // https://developer.android.com/reference/android/hardware/display/VirtualDisplay.Callback + // https://learn.microsoft.com/en-us/dotnet/api/android.hardware.display.virtualdisplay.callback.onpaused?view=net-android-34.0 + virtualDisplay?.setSurface(null) + } else { + virtualDisplay?.release() + } + // suface needs to be release after `imageReader.close()` to imageReader access released surface + // https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629 + imageReader?.close() + imageReader = null + videoEncoder?.let { + it.signalEndOfInputStream() + it.stop() + it.release() + } + if (!reuseVirtualDisplay) { + virtualDisplay = null + } + videoEncoder = null + // suface needs to be release after `imageReader.close()` to imageReader access released surface + // https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629 + surface?.release() + + // release audio + _isAudioStart = false + audioRecordHandle.tryReleaseAudio() + } + + fun destroy() { + Log.d(logTag, "destroy service") + _isReady = false + _isAudioStart = false + + stopCapture() + + if (reuseVirtualDisplay) { + virtualDisplay?.release() + virtualDisplay = null + } + + mediaProjection = null + checkMediaPermission() + stopForeground(true) + stopService(Intent(this, FloatingWindowService::class.java)) + stopSelf() + } + + fun checkMediaPermission(): Boolean { + Handler(Looper.getMainLooper()).post { + MainActivity.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "media", "value" to isReady.toString()) + ) + } + Handler(Looper.getMainLooper()).post { + MainActivity.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "input", "value" to InputService.isOpen.toString()) + ) + } + return isReady + } + + private fun startRawVideoRecorder(mp: MediaProjection) { + Log.d(logTag, "startRawVideoRecorder,screen info:$SCREEN_INFO") + if (surface == null) { + Log.d(logTag, "startRawVideoRecorder failed,surface is null") + return + } + createOrSetVirtualDisplay(mp, surface!!) + } + + private fun startVP9VideoRecorder(mp: MediaProjection) { + createMediaCodec() + videoEncoder?.let { + surface = it.createInputSurface() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + surface!!.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT) + } + it.setCallback(cb) + it.start() + createOrSetVirtualDisplay(mp, surface!!) + } + } + + // https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250 + // Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection. + private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) { + try { + virtualDisplay?.let { + it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi) + it.setSurface(s) + } ?: let { + virtualDisplay = mp.createVirtualDisplay( + "RustDeskVD", + SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, + s, null, null + ) + } + } catch (e: SecurityException) { + Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation"); + // This initiates a prompt dialog for the user to confirm screen projection. + requestMediaProjection() + } + } + + private val cb: MediaCodec.Callback = object : MediaCodec.Callback() { + override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {} + override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {} + + override fun onOutputBufferAvailable( + codec: MediaCodec, + index: Int, + info: MediaCodec.BufferInfo + ) { + codec.getOutputBuffer(index)?.let { buf -> + sendVP9Thread.execute { + val byteArray = ByteArray(buf.limit()) + buf.get(byteArray) + // sendVp9(byteArray) + codec.releaseOutputBuffer(index, false) + } + } + } + + override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { + Log.e(logTag, "MediaCodec.Callback error:$e") + } + } + + private fun createMediaCodec() { + Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE") + videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE) + val mFormat = + MediaFormat.createVideoFormat(MIME_TYPE, SCREEN_INFO.width, SCREEN_INFO.height) + mFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_KEY_BIT_RATE) + mFormat.setInteger(MediaFormat.KEY_FRAME_RATE, VIDEO_KEY_FRAME_RATE) + mFormat.setInteger( + MediaFormat.KEY_COLOR_FORMAT, + MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible + ) + mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5) + try { + videoEncoder!!.configure(mFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) + } catch (e: Exception) { + Log.e(logTag, "mEncoder.configure fail!") + } + } + + private fun initNotification() { + notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channelId = "RustDesk" + val channelName = "RustDesk Service" + val channel = NotificationChannel( + channelId, + channelName, NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "RustDesk Service Channel" + } + channel.lightColor = Color.BLUE + channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + notificationManager.createNotificationChannel(channel) + channelId + } else { + "" + } + notificationBuilder = NotificationCompat.Builder(this, notificationChannel) + } + + @SuppressLint("UnspecifiedImmutableFlag") + private fun createForegroundNotification() { + val intent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + action = Intent.ACTION_MAIN + addCategory(Intent.CATEGORY_LAUNCHER) + putExtra("type", type) + } + val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.getActivity(this, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + } else { + PendingIntent.getActivity(this, 0, intent, FLAG_UPDATE_CURRENT) + } + val notification = notificationBuilder + .setOngoing(true) + .setSmallIcon(R.mipmap.ic_stat_logo) + .setDefaults(Notification.DEFAULT_ALL) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentTitle(DEFAULT_NOTIFY_TITLE) + .setContentText(translate(DEFAULT_NOTIFY_TEXT)) + .setOnlyAlertOnce(true) + .setContentIntent(pendingIntent) + .setColor(ContextCompat.getColor(this, R.color.primary)) + .setWhen(System.currentTimeMillis()) + .build() + startForeground(DEFAULT_NOTIFY_ID, notification) + } + + private fun loginRequestNotification( + clientID: Int, + type: String, + username: String, + peerId: String + ) { + val notification = notificationBuilder + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentTitle(translate("Do you accept?")) + .setContentText("$type:$username-$peerId") + // .setStyle(MediaStyle().setShowActionsInCompactView(0, 1)) + // .addAction(R.drawable.check_blue, "check", genLoginRequestPendingIntent(true)) + // .addAction(R.drawable.close_red, "close", genLoginRequestPendingIntent(false)) + .build() + notificationManager.notify(getClientNotifyID(clientID), notification) + } + + private fun onClientAuthorizedNotification( + clientID: Int, + type: String, + username: String, + peerId: String + ) { + cancelNotification(clientID) + val notification = notificationBuilder + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentTitle("$type ${translate("Established")}") + .setContentText("$username - $peerId") + .build() + notificationManager.notify(getClientNotifyID(clientID), notification) + } + + private fun voiceCallRequestNotification( + clientID: Int, + type: String, + username: String, + peerId: String + ) { + val notification = notificationBuilder + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentTitle(translate("Do you accept?")) + .setContentText("$type:$username-$peerId") + .build() + notificationManager.notify(getClientNotifyID(clientID), notification) + } + + private fun getClientNotifyID(clientID: Int): Int { + return clientID + NOTIFY_ID_OFFSET + } + + fun cancelNotification(clientID: Int) { + notificationManager.cancel(getClientNotifyID(clientID)) + } + + @SuppressLint("UnspecifiedImmutableFlag") + private fun genLoginRequestPendingIntent(res: Boolean): PendingIntent { + val intent = Intent(this, MainService::class.java).apply { + action = ACT_LOGIN_REQ_NOTIFY + putExtra(EXT_LOGIN_REQ_NOTIFY, res) + } + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.getService(this, 111, intent, FLAG_IMMUTABLE) + } else { + PendingIntent.getService(this, 111, intent, FLAG_UPDATE_CURRENT) + } + } + + private fun setTextNotification(_title: String?, _text: String?) { + val title = _title ?: DEFAULT_NOTIFY_TITLE + val text = _text ?: translate(DEFAULT_NOTIFY_TEXT) + val notification = notificationBuilder + .clearActions() + .setStyle(null) + .setContentTitle(title) + .setContentText(text) + .build() + notificationManager.notify(DEFAULT_NOTIFY_ID, notification) + } +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt new file mode 100644 index 0000000..3beb7ec --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt @@ -0,0 +1,54 @@ +package com.carriez.flutter_hbb + +import android.app.Activity +import android.content.Intent +import android.media.projection.MediaProjectionManager +import android.os.Build +import android.os.Bundle +import android.util.Log + +class PermissionRequestTransparentActivity: Activity() { + private val logTag = "permissionRequest" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.d(logTag, "onCreate PermissionRequestTransparentActivity: intent.action: ${intent.action}") + + when (intent.action) { + ACT_REQUEST_MEDIA_PROJECTION -> { + val mediaProjectionManager = + getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + val intent = mediaProjectionManager.createScreenCaptureIntent() + startActivityForResult(intent, REQ_REQUEST_MEDIA_PROJECTION) + } + else -> finish() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQ_REQUEST_MEDIA_PROJECTION) { + if (resultCode == RESULT_OK && data != null) { + launchService(data) + } else { + setResult(RES_FAILED) + } + } + + finish() + } + + private fun launchService(mediaProjectionResultIntent: Intent) { + Log.d(logTag, "Launch MainService") + val serviceIntent = Intent(this, MainService::class.java) + serviceIntent.action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE + serviceIntent.putExtra(EXT_MEDIA_PROJECTION_RES_INTENT, mediaProjectionResultIntent) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent) + } else { + startService(serviceIntent) + } + } + +} \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt new file mode 100644 index 0000000..8c9d850 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/RdClipboardManager.kt @@ -0,0 +1,197 @@ +package com.carriez.flutter_hbb + +import java.nio.ByteBuffer +import java.util.Timer +import java.util.TimerTask + +import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipboardManager +import android.util.Log +import androidx.annotation.Keep + +import hbb.MessageOuterClass.ClipboardFormat +import hbb.MessageOuterClass.Clipboard +import hbb.MessageOuterClass.MultiClipboards + +import ffi.FFI + +class RdClipboardManager(private val clipboardManager: ClipboardManager) { + private val logTag = "RdClipboardManager" + private val supportedMimeTypes = arrayOf( + ClipDescription.MIMETYPE_TEXT_PLAIN, + ClipDescription.MIMETYPE_TEXT_HTML + ) + + // 1. Avoid listening to the same clipboard data updated by `rustUpdateClipboard`. + // 2. Avoid sending the clipboard data before enabling client clipboard. + // 1) Disable clipboard + // 2) Copy text "a" + // 3) Enable clipboard + // 4) Switch to another app + // 5) Switch back to the app + // 6) "a" should not be sent to the client, because it's copied before enabling clipboard + // + // It's okay to that `rustEnableClientClipboard(false)` is called after `rustUpdateClipboard`, + // though the `lastUpdatedClipData` will be set to null once. + private var lastUpdatedClipData: ClipData? = null + private var isClientEnabled = true; + private var _isCaptureStarted = false; + + val isCaptureStarted: Boolean + get() = _isCaptureStarted + + fun checkPrimaryClip(isClient: Boolean) { + val clipData = clipboardManager.primaryClip + if (clipData != null && clipData.itemCount > 0) { + // Only handle the first item in the clipboard for now. + val clip = clipData.getItemAt(0) + // Ignore the `isClipboardDataEqual()` check if it's a host operation. + // Because it's an action manually triggered by the user. + if (isClient) { + if (lastUpdatedClipData != null && isClipboardDataEqual(clipData, lastUpdatedClipData!!)) { + Log.d(logTag, "Clipboard data is the same as last update, ignore") + return + } + } + val mimeTypeCount = clipData.description.getMimeTypeCount() + val mimeTypes = mutableListOf() + for (i in 0 until mimeTypeCount) { + mimeTypes.add(clipData.description.getMimeType(i)) + } + var text: CharSequence? = null; + var html: String? = null; + if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { + text = clip?.text + } + if (isSupportedMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) { + text = clip?.text + html = clip?.htmlText + } + var count = 0 + val clips = MultiClipboards.newBuilder() + if (text != null) { + val content = com.google.protobuf.ByteString.copyFromUtf8(text.toString()) + clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Text).setContent(content).build()) + count++ + } + if (html != null) { + val content = com.google.protobuf.ByteString.copyFromUtf8(html) + clips.addClipboards(Clipboard.newBuilder().setFormat(ClipboardFormat.Html).setContent(content).build()) + count++ + } + if (count > 0) { + val clipsBytes = clips.build().toByteArray() + val isClientFlag = if (isClient) 1 else 0 + val clipsBuf = ByteBuffer.allocateDirect(clipsBytes.size + 1).apply { + put(isClientFlag.toByte()) + put(clipsBytes) + } + clipsBuf.flip() + lastUpdatedClipData = clipData + Log.d(logTag, "${if (isClient) "client" else "host"}, send clipboard data to the remote") + FFI.onClipboardUpdate(clipsBuf) + } + } + } + + private fun isSupportedMimeType(mimeType: String): Boolean { + return supportedMimeTypes.contains(mimeType) + } + + private fun isClipboardDataEqual(left: ClipData, right: ClipData): Boolean { + if (left.description.getMimeTypeCount() != right.description.getMimeTypeCount()) { + return false + } + val mimeTypeCount = left.description.getMimeTypeCount() + for (i in 0 until mimeTypeCount) { + if (left.description.getMimeType(i) != right.description.getMimeType(i)) { + return false + } + } + + if (left.itemCount != right.itemCount) { + return false + } + for (i in 0 until left.itemCount) { + val mimeType = left.description.getMimeType(i) + if (!isSupportedMimeType(mimeType)) { + continue + } + val leftItem = left.getItemAt(i) + val rightItem = right.getItemAt(i) + if (mimeType == ClipDescription.MIMETYPE_TEXT_PLAIN || mimeType == ClipDescription.MIMETYPE_TEXT_HTML) { + if (leftItem.text != rightItem.text || leftItem.htmlText != rightItem.htmlText) { + return false + } + } + } + return true + } + + fun setCaptureStarted(started: Boolean) { + _isCaptureStarted = started + } + + @Keep + fun rustEnableClientClipboard(enable: Boolean) { + Log.d(logTag, "rustEnableClientClipboard: enable: $enable") + isClientEnabled = enable + lastUpdatedClipData = null + } + + fun syncClipboard(isClient: Boolean) { + Log.d(logTag, "syncClipboard: isClient: $isClient, isClientEnabled: $isClientEnabled") + if (isClient && !isClientEnabled) { + return + } + checkPrimaryClip(isClient) + } + + @Keep + fun rustUpdateClipboard(clips: ByteArray) { + val clips = MultiClipboards.parseFrom(clips) + var mimeTypes = mutableListOf() + var text: String? = null + var html: String? = null + for (clip in clips.getClipboardsList()) { + when (clip.format) { + ClipboardFormat.Text -> { + mimeTypes.add(ClipDescription.MIMETYPE_TEXT_PLAIN) + text = String(clip.content.toByteArray(), Charsets.UTF_8) + } + ClipboardFormat.Html -> { + mimeTypes.add(ClipDescription.MIMETYPE_TEXT_HTML) + html = String(clip.content.toByteArray(), Charsets.UTF_8) + } + ClipboardFormat.ImageRgba -> { + } + ClipboardFormat.ImagePng -> { + } + else -> { + Log.e(logTag, "Unsupported clipboard format: ${clip.format}") + } + } + } + + val clipDescription = ClipDescription("clipboard", mimeTypes.toTypedArray()) + var item: ClipData.Item? = null + if (text == null) { + Log.e(logTag, "No text content in clipboard") + return + } else { + if (html == null) { + item = ClipData.Item(text) + } else { + item = ClipData.Item(text, html) + } + } + if (item == null) { + Log.e(logTag, "No item in clipboard") + return + } + val clipData = ClipData(clipDescription, item) + lastUpdatedClipData = clipData + clipboardManager.setPrimaryClip(clipData) + } +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/VolumeController.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/VolumeController.kt new file mode 100644 index 0000000..be30b65 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/VolumeController.kt @@ -0,0 +1,78 @@ +package com.carriez.flutter_hbb + +// Inspired by https://github.com/yosemiteyss/flutter_volume_controller/blob/main/android/src/main/kotlin/com/yosemiteyss/flutter_volume_controller/VolumeController.kt + +import android.media.AudioManager +import android.os.Build +import android.util.Log + +class VolumeController(private val audioManager: AudioManager) { + private val logTag = "volume controller" + + fun getVolume(streamType: Int): Double { + val current = audioManager.getStreamVolume(streamType) + val max = audioManager.getStreamMaxVolume(streamType) + return current.toDouble() / max + } + + fun setVolume(volume: Double, showSystemUI: Boolean, streamType: Int) { + val max = audioManager.getStreamMaxVolume(streamType) + audioManager.setStreamVolume( + streamType, + (max * volume).toInt(), + if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0 + ) + } + + fun raiseVolume(step: Double?, showSystemUI: Boolean, streamType: Int) { + if (step == null) { + audioManager.adjustStreamVolume( + streamType, + AudioManager.ADJUST_RAISE, + if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0 + ) + } else { + val target = getVolume(streamType) + step + setVolume(target, showSystemUI, streamType) + } + } + + fun lowerVolume(step: Double?, showSystemUI: Boolean, streamType: Int) { + if (step == null) { + audioManager.adjustStreamVolume( + streamType, + AudioManager.ADJUST_LOWER, + if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0 + ) + } else { + val target = getVolume(streamType) - step + setVolume(target, showSystemUI, streamType) + } + } + + fun getMute(streamType: Int): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + audioManager.isStreamMute(streamType) + } else { + audioManager.getStreamVolume(streamType) == 0 + } + } + + private fun setMute(isMuted: Boolean, showSystemUI: Boolean, streamType: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + audioManager.adjustStreamVolume( + streamType, + if (isMuted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE, + if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0 + ) + } else { + audioManager.setStreamMute(streamType, isMuted) + } + } + + fun toggleMute(showSystemUI: Boolean, streamType: Int) { + val isMuted = getMute(streamType) + setMute(!isMuted, showSystemUI, streamType) + } +} + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt new file mode 100644 index 0000000..514d493 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -0,0 +1,157 @@ +package com.carriez.flutter_hbb + +import android.Manifest.permission.* +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.media.AudioRecord +import android.media.AudioRecord.READ_BLOCKING +import android.media.MediaCodecList +import android.media.MediaFormat +import android.net.Uri +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.PowerManager +import android.provider.Settings +import android.provider.Settings.* +import android.util.DisplayMetrics +import android.util.Log +import android.view.WindowManager +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat.getSystemService +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions +import ffi.FFI +import java.nio.ByteBuffer +import java.util.* + + +// intent action, extra +const val ACT_REQUEST_MEDIA_PROJECTION = "REQUEST_MEDIA_PROJECTION" +const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SERVICE" +const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY" +const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT" +const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT" +const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY" + +// Activity requestCode +const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101 +const val REQ_REQUEST_MEDIA_PROJECTION = 201 + +// Activity responseCode +const val RES_FAILED = -100 + +// Flutter channel +const val START_ACTION = "start_action" +const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt" +const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt" +const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir" +const val GET_VALUE = "get_value" + +const val KEY_IS_SUPPORT_VOICE_CALL = "KEY_IS_SUPPORT_VOICE_CALL" + +const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES" +const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT" +const val KEY_APP_DIR_CONFIG_PATH = "KEY_APP_DIR_CONFIG_PATH" + +@SuppressLint("ConstantLocale") +val LOCAL_NAME = Locale.getDefault().toString() +val SCREEN_INFO = Info(0, 0, 1, 200) + +data class Info( + var width: Int, var height: Int, var scale: Int, var dpi: Int +) + +fun isSupportVoiceCall(): Boolean { + // https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R +} + +fun requestPermission(context: Context, type: String) { + XXPermissions.with(context) + .permission(type) + .request { _, all -> + if (all) { + Handler(Looper.getMainLooper()).post { + MainActivity.flutterMethodChannel?.invokeMethod( + "on_android_permission_result", + mapOf("type" to type, "result" to all) + ) + } + } + } +} + +fun startAction(context: Context, action: String) { + try { + context.startActivity(Intent(action).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // don't pass package name when launch ACTION_ACCESSIBILITY_SETTINGS + if (ACTION_ACCESSIBILITY_SETTINGS != action) { + data = Uri.parse("package:" + context.packageName) + } + }) + } catch (e: Exception) { + e.printStackTrace() + } +} + +class AudioReader(val bufSize: Int, private val maxFrames: Int) { + private var currentPos = 0 + private val bufferPool: Array + + init { + if (maxFrames < 0 || maxFrames > 32) { + throw Exception("Out of bounds") + } + if (bufSize <= 0) { + throw Exception("Wrong bufSize") + } + bufferPool = Array(maxFrames) { + ByteBuffer.allocateDirect(bufSize) + } + } + + private fun next() { + currentPos++ + if (currentPos >= maxFrames) { + currentPos = 0 + } + } + + @RequiresApi(Build.VERSION_CODES.M) + fun readSync(audioRecord: AudioRecord): ByteBuffer? { + val buffer = bufferPool[currentPos] + val res = audioRecord.read(buffer, bufSize, READ_BLOCKING) + return if (res > 0) { + next() + buffer + } else { + null + } + } +} + + +fun getScreenSize(windowManager: WindowManager) : Pair{ + var w = 0 + var h = 0 + @Suppress("DEPRECATION") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val m = windowManager.maximumWindowMetrics + w = m.bounds.width() + h = m.bounds.height() + } else { + val dm = DisplayMetrics() + windowManager.defaultDisplay.getRealMetrics(dm) + w = dm.widthPixels + h = dm.heightPixels + } + return Pair(w, h) +} + + fun translate(input: String): String { + Log.d("common", "translate:$LOCAL_NAME") + return FFI.translateLocale(LOCAL_NAME, input) +} \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/ffi.kt b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/ffi.kt new file mode 100644 index 0000000..e3c9d98 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/kotlin/ffi.kt @@ -0,0 +1,30 @@ +// ffi.kt + +package ffi + +import android.content.Context +import java.nio.ByteBuffer + +import com.carriez.flutter_hbb.RdClipboardManager + +object FFI { + init { + System.loadLibrary("rustdesk") + } + + external fun init(ctx: Context) + external fun onAppStart(ctx: Context) + external fun setClipboardManager(clipboardManager: RdClipboardManager) + external fun startServer(app_dir: String, custom_client_config: String) + external fun startService() + external fun onVideoFrameUpdate(buf: ByteBuffer) + external fun onAudioFrameUpdate(buf: ByteBuffer) + external fun translateLocale(localeName: String, input: String): String + external fun refreshScreen() + external fun setFrameRawEnable(name: String, value: Boolean) + external fun setCodecInfo(info: String) + external fun getLocalOption(key: String): String + external fun getBuildinOption(key: String): String + external fun onClipboardUpdate(clips: ByteBuffer) + external fun isServiceClipboardEnabled(): Boolean +} diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable-v21/launch_background.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/check_blue.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/check_blue.xml new file mode 100644 index 0000000..b06974b --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/check_blue.xml @@ -0,0 +1,5 @@ + + + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/close_red.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/close_red.xml new file mode 100644 index 0000000..02ff2c8 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/close_red.xml @@ -0,0 +1,5 @@ + + + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/floating_window.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/floating_window.xml new file mode 100644 index 0000000..d22152d --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/floating_window.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/launch_background.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..65291b9 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..65291b9 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values-night/styles.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..449a9f9 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/colors.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..2734689 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FF0071FF + \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/ic_launcher_background.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..ab98328 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #ffffff + \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/strings.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3e058a8 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + RustDesk + Allow other devices to control your phone using virtual touch, when RustDesk screen sharing is established + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/styles.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..146267c --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/values/styles.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/xml/accessibility_service_config.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/xml/accessibility_service_config.xml new file mode 100644 index 0000000..90b57cd --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/main/res/xml/accessibility_service_config.xml @@ -0,0 +1,7 @@ + diff --git a/shelled/rustdesk-as-ref/flutter/android/app/src/profile/AndroidManifest.xml b/shelled/rustdesk-as-ref/flutter/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..64d68a5 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/shelled/rustdesk-as-ref/flutter/android/build.gradle b/shelled/rustdesk-as-ref/flutter/android/build.gradle new file mode 100644 index 0000000..401bea0 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/build.gradle @@ -0,0 +1,19 @@ +allprojects { + repositories { + google() + jcenter() + maven { url 'https://jitpack.io' } + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/shelled/rustdesk-as-ref/flutter/android/gradle.properties b/shelled/rustdesk-as-ref/flutter/android/gradle.properties new file mode 100644 index 0000000..804b29b --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1024M +android.useAndroidX=true +android.enableJetifier=true +org.gradle.daemon=false diff --git a/shelled/rustdesk-as-ref/flutter/android/gradle/wrapper/gradle-wrapper.properties b/shelled/rustdesk-as-ref/flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cb57630 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip diff --git a/shelled/rustdesk-as-ref/flutter/android/settings.gradle b/shelled/rustdesk-as-ref/flutter/android/settings.gradle new file mode 100644 index 0000000..ae32fa0 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.1" apply false + id "org.jetbrains.kotlin.android" version "2.1.21" apply false +} + +include ":app" diff --git a/shelled/rustdesk-as-ref/flutter/assets/address_book.ttf b/shelled/rustdesk-as-ref/flutter/assets/address_book.ttf new file mode 100644 index 0000000000000000000000000000000000000000..509fb63c09da8c63d47908d675ecfca1f703d948 GIT binary patch literal 1792 zcmd^9T~8cU7=F%~VL`wOmV%&JhL2q$g<064lvIR5Es*%ZM-eqK%CI}TP3gpOxr7Zp_lc_06u5-tRJ-hz%z5+^Ssadao+RJ z$v^;TK@|qZ7B9U%Z+(!s4u~nT>&ur?>FKVepQwLGeKS*Z%MXUX`hxmH&T_u6m;3q4 z)lUJVK>NdO&&>|r-guArKIh~%iN?FeP2z7j2e*siwI%|_{!Y`)r!~nVW#jm;FjO{N@sEvF~Z01Zy{{M^(ISQl%FIBk+%q5kvOn zw{C2{`_%LwXsP+$|N53XBWml>JrhQPY7?XmR+%K$&=Bd0ZRP_UMS5+gW{hD5@}-4W z)^VcXh9&fa`Oz}ev~D<<#S&I&9_SzF>pvk* zwRgmY)7SZeNY98f?Gv^T6O(6cab{vhq~qdLN31cDbWS!mRV9Unft zVXrtbQEGHnMh6^c(Q%By_(c5R6O|%yJ}Fbl^NEA&%hhgJ!+yhJ0O)z~24udzuy ztZ@VXQfZB2+6OWRCfH5HExg9 zsYS2k1@$zQtNC;o=B!-c7p(>6^9lvuDhK{$FB96^VOTzwN~wJmZ)f}>E+Bvh7a?U9 z7PhcQxq}RRl#q*L_=vtn?>w%cz{ubc>+$FfdIzY`Pvx{Q4x7Ke<6evODn*{htEu2B zcT2OzkQG^+0W%ee7Ib|cD=83L{D8`KnRznwwy_;Wm2nO!KK1j|{q5+SMfG#64fT(H X+>k;F1Fdd08+et<=$7yAD!0D?EMoMM literal 0 HcmV?d00001 diff --git a/shelled/rustdesk-as-ref/flutter/assets/device_group.ttf b/shelled/rustdesk-as-ref/flutter/assets/device_group.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a6e42704f06e15332067635d9ce5521b90ce46ef GIT binary patch literal 2012 zcmd^ATW=dh6h1TSOJX}|ZO0C7op^1xPTDkf;#;bchCpeOA`rA`B8m`+>#Q%8?X{du zl^`B0kPxUyARa0q@x}{~2QDwD5)Tmq6;y;`~c4G z%=ykY=bV{2yR%D-h@8|UiN?0=q!9R_>)$@xTb^6+D$uwR3{oUi8UTw{q~yJ0HXM23ocR!oDqi4*V(FzEV|NcTEEk`xTw5 zqULjSk-kBE6LwcMw_ay%+Q9e&u;p5=s_cGe@R&rX6_L%F7OK$E@Dw{}?*VmU0{!2`WfWl8ECZ@?Gum#V zvO9C__UF4M&H$c3>x#6Ew=hj}v`iILGC9aX%ook1V+j_H*z7)c*cTRD*cbJseBnqe zoqdR$71v=1uzBAe~L| zFXU{5v#C(O{q7I_QHfpBXZfJt&R&?B(r@&5b~&wY-40E_C6to8`Ssv<5cfv|V?qr@ zgF{2XcSRR**dMk#p6PbEx=)IZ|2CKob&rnrM-ka?*S|aAlQ}zj=7c}k$JrSSI#@k4 z5teq4N52pp8$e371|xPLsJ|H;9uBfv@IaT#rJrfOPW?cl1PYo1Ru|?f+P~~m@GjCCj!F*k|K+_1?*^^HPvn$ooZ|R=-y1axubSa`?`l@* zJZ4K`jT%-YqcxDJ3N&M`Pr*tmz%m}77(0hNd3eXEWJJ~J2qkcJ_S5l?8~d!b|1#nY m6aSg}iNrXQn1y;!v-+B*H0mX_rg%Hnl3^|t$`yP(Ab$W9?kvs# literal 0 HcmV?d00001 diff --git a/shelled/rustdesk-as-ref/flutter/assets/gestures.ttf b/shelled/rustdesk-as-ref/flutter/assets/gestures.ttf new file mode 100644 index 0000000000000000000000000000000000000000..aabec8ac9e67bc0c79be3b4b15224c8c99ed5b16 GIT binary patch literal 8068 zcmd^EYj9mxb>5HjexG~qIp@CLx=-oK_w87+tT>XB;5e>djP0b@vMftVBukNH2q^<5 zC6E+K%1o!zw9v`W*OUMur9fLKW#}*r15Rf;v}sF88K$F%Q`lF4nyY{(9mg59E zwEXLl_StK%z0N-S?!DHx*4hVQgpi7^Ac}U~dF;gg?E6O^K?v^#?(xIN##@sgyyv%| z{Q+p7onM_>Kl%&b_&7qO2<5KDmA7B`m+0455#pfVdw6MK?tJ$AvlL{%1EseF4Dlc7 z2Z8@0w!iSV@cat2)2nk2uj4Fw75byl&R(2bU9f(8 z@>2*UIe6dV`r4(<|N4IU*FhBIv5R2t^vd-s=zb1Hy?&j;jPshy@HxPJ<&p0;&X|)w zLMjXe*NY$h%!jsXcyyk_^xe>w5Mf-n*hmKYH{h^~aTWMO2xjkJtY1O9&=evw2ktqJ zjFq{~iztqU!w;XonZ_&2i*x>H>`4rAtQP|ZBf+O&890Q01@Z8@C&)K|_vEm~|BPA~ z=0On8*7gmqpSv)79(`%+{+FNk*8|aiKpz1y_NJYSuou3*z`DI5e@7O&VNReS2#pR! z@rQ%4e+{_oEu&Dw>HwcZ4?x?3Kak(Le`{rHZR>qoPi}qa<>%SX1LpzYJht^g;9O;# z*Id|tGJ+x~3TqjMJ_0Kw!wQKoJ}eLDs)lr!4~&l4y8VB@G=Bj;9xQ{-R(M#L&The8 zu)_3i>%n1x)kIq>!@@LgYi(Fyh0)ey!@~A*>%GImRA%da!@~A@3+$N{*a@`tCMZ}4-4Bdxc&;4NS;Qy4sYx|=4-o!%04YS%Vz{~PN1O^3} zUWW=o)^p`rDjKTosd|xAmD*g|EtTrFk}2LE)2w<}%k9mjtW+jOW4aeLEzhzY7w)cO z+pcBTIo`I!h$Y4KIxl&We(e!iNY)dgt&7uw4Mo2Tszg00*t#$++EDPJbe?=7RW<9W zYBi;r)zsjfTtphR6K-sFq0Gs>EBIq#;@Y)@sI&L8m+FHbh++Z*-Z3U{fFFiEf*rj+ zPrpW9L1C0Y8B|7X*r#0Hz>XbB<94n!g}ZjAfyH(!jZ-*>b9Qc|GleIxox>3W7jpRq zNaV;%gZAE~$;qX?`0C!py?Ynmb7$|q?%;=mzr)(#mBHWl?(fbFend}q@9PcPz59Cf zH2EP2!$VA%J+w3#2mTQp9enQy5QFax-UB0mJf1^Lo3FpiNA(6;N56`mMjuC?Kz{^3 zAeS$7JG}{9E_M1-xZTQt225dZ*zkZRjF-kv1TNqcm?0ukvuriT#au82f9K5?=dxuXttf`TE2}ouJVP#gr?l0ZLXXNjxhm zia59+$+Coh1L&&HV7I|bwjB!DS7d2)&oiNJNRsVQQ&MH>*pe(Nf@I3}OzuQu;zBx; z@1E|I-jtk?teC_b3h!`4kUYgNk=V*^-}-O$BB-3k4`Tt0+oX0ni%i=jZ5`=}~kRZKB7~hY`dN3`g=}18?JqGlUV? zU-)wXMtscJ29Z*qDMY{N+<=a42m!rmxhdT4_HG4`cDK-no$`@`VMVV~=7BL(n+z^E zZBQ7uRqnqIW?G)uEjX_MnNprUD`ui$8CO!KUfomE%v3GynTBo%F{3P4qEgpoFK>E+ zrYX8pw_UroU6h;{HszRJ=cK4;y!wShhjal9xG92K0-oS}Q-Fr82|DZ1?9Xd;opk^Y zif-Sn*A)guAPq$i`_MyDW#PLFdIUh8Eoy1SbdNOq*&b}7E!m!(9!rmx8mWZNui|HY zu=x_c;@4Nsvg)B3A^Vo40B=rBK8PQA?OVlk@IG76>l(cOYx<#sI)fva?l`os`P1F` zA#ATDh}_;quR-L-P-q`I2s{2~=!@tp5RDdmlmXmf7M8-y?)GBe?gFF=EbJ!Bqc_tx zTOqUHPH!K^V3^x*;bR84j%}zgv4NSj14#I$Ul8FvzJ0@ky#PUg7Xe)pVYdS^0F+yr z;lS>{P%Yn4EzeYNb?-sY^WBZX3z>E%(@BpCl7iQ-y~vD{3N!r&GB?ekOTz;K(+o>v)LNS(a@d;e@rW@YTh~r_$fmMt| zBGKq`nWNRl@!V{#K=z0t%wX{6ROXdD%{YpYh8Z3mnN2*aX+p@dc^Vznbv|T;xG@GN zr~HP^pJFKUf`}gil!(X>{y@}o{W&^9*C1|*p%Ut#X~eRehzm*qR?J|#-0CrXVKGO< z6^FcGf8P%ThNkYhh3CBdT_8hx1iQ?1D9mT~VZ;XZG6_LYhF3kc~R3P~2rH=?p+gwUe$8A!g-hNK$J6 zA{jHv5!b5Tzz@KYx_kpeR8Q2&6N;vVHEmu~4$GP*ACc7+sKc6Zuh3)A0XI<)7|Qh6 zZ7>bN3g@PDu-?B&)@sSYw@j&>y(8RgrN+6a7E;1qC|OI6)$Hj|idP=Rf8!&?Z=TaE zOFMUj-OtVnH{z4>smUc4&0haBjnQ#*9=&}B(SxO)!Vqyl3=(lY=f^L2lNs?(vErZx zDcI94pa{e=EFJ(mrrga-sugSH*`qU+da;>-g^ine8WPpIuDDqv<}YmBf(3=>&o4?A zzMMZ7!fmAgVN;W5&$%s|M2X~h3aQLocGnLBBu{$HrvbEbEX*N^4ZeF{(J#G7I z_syTUcKK6#AHe@qOHLcLSgM&!HdE2MF`caWaokfhOPA4;=y%Z{qi>>rg;gyTIxMh; zQeZBy*B}OjF|J2}Q|^?yQxG+kA+|F}8vFX}-$buhW)V_w>-XB=9{nB`h===S7z@H7 zn!!F{5^q>z7K*pQG?*WTm>VjOp(XfV7;wZO5aEMKm7tlXBD@!uK&%802hWJT(uf1G zR@-A?TB%SDd@zUt`g)?5hOiUjw{o{LPD%!L+n(=+`{W0zlk`F{Gpj@jv5IPOrG2w| ztdJUiP?0r}7@}nDa=Y=|xaoBBqL>ZUa6(r3svJ^t+=v+$WDaA(skX}VBF}}3;jsP= zLR1=YReIVD$7v|4LTXU4IXed2s;U}TpaNCesoQ8oP)L=e=w!C9a+Z4Sc_XgFaG`{t z$qy2uNkmd5 zUbtH_i8kyN@B!W{3%r<@=p`v1%^cX1Penh$vf_!_j`cnb0{&ZO_c?lE!aq zrABc=51C3j9m#Q}SatkJT(LMo<4sEyONM|6UL_=}szO*9d|ea{0~JxHY;{W2#E3eh z);W#}a_IPpU3YMvY$%G5qX&BT-CuOgW7u*+Mp8)A zs@Y*5Zo+nzT&XiK-`$mETN=D_EB5qiUc5MU_f)Z-Pgwsq4D#|Pcb~(xXuVa7Mr*Nh z_qjK~BbNJrl0HgaMrY6^h}-{nG5Zd<|IZ*bvx^)n#9t68_#6&n4~J=cT%-lO01;@FL9Pu$go&D)01z++L=bFk&YYO zDrES!tq#73ANA{Zo@LcT2gK}|rHLT7pIpH&vg957bn-eJ;3Rz)eKk24Bn2%bOhdL0 zk%I4EKSwL{4)iGcotszRWT4#z_sVPq@+Lp5&G=ybV{#{k-_334rNeM0oQBA%@1MYU zR7hihc|YI*jAm%h97f;-*qIEo9-0TN2cH9ZGB^R|gIQV*a{*Tjp19vDQ3pJ6SR$2_ zsB!9OuR2>P?sp2s>P)3mv+b~DXoeUwOQI!!M-K8ORS`u=Cn{j2)&#daPE<=HK$*CS zRofsoBh9)>WL2?5gXb(cjP05%$N)kAo~)2`9KtBciKd&$k%VLF-N|M$;i|Z@ohea# z%sR9SW>Lu4aBmz8Jw42rXtS5?$*y9{A5fbIP~1GIt2Cs?`|4AKh~b*+SybVuBA6;6a#`nyVem2)1q*hL zBj86g_se`l6=X`6E2U~cW;wC!=tNhbgX5~@v@Kb3bRs|Hf*zHKr|vKAi^-Zi=q^8W zoGK837!md9SkiQOJ(3uFE}~dU z5%?sr|4n;Zxk^qCTXr(#6lf(e)_hBmaDpycvQrL2j*g#JVA8s(e(R)tQW1Fq>CypW z+O@POanPZtC+#UtN*+5rHs#2=WOJ^zfBeMBY}CR}^3{-@W;Q-NF9eK<8R1d8*d8gx zB~62=LcYu}_Cxeux(a6_zr?=D3|->R`RdR7T!gx?+1ud3kR$NLe)2<@Nwc^bB21on zXgIxPU=T!r5cEL+2vEVo&;T5uf+!9`OS%U@*_A3uQ?K1t)6HZp>zL*n*}DqoBirFO z9KY5f1ZQWjT8&pcN&75hzHul zrF0~oY@P1mIM3-}K1*(2+gHfPGrK$$w!n4MRK`{CrX*1ozk375X?O_Z3;-C6GXNOP zG|FRXh`m5SE67$=!4PYOg&mr~7C zE-BmZl|y3s-a|>vkj#)SMOBjuwc}^%lCAJU_u-|E$c6P-$(BvCcxJAQ;W(FFA@rjE z9f#5Ely~^$h<%+32Q+~b)kHv3;O7IH1G*N_JbV*q1+)PCy#cMDI+_h=6-HbKO~f1| z;*fj)VnDMmKYtL=1Y)>931|xZuLd*+^a}yaBNx3C&;szU1+;?RjP-z4Q3=oAv9U0> zxo|#v?(Ny-`L&A|)-G;t*GCr?FR#pPY&W;-H!o~lT3)-DZ8mmov+i8DxUjK3%%!(3 zwl+5}WG`&2t!59vcnd2lYuWXUwFegFHycZvo9lbV$Ju)rzcIhIita!gXaUW^@zesO zuUT{sQrIk7M)PP5T|^iB`{wJ~M?rcKT}CVLe}T?3)=Cyb1Ms*W5?Gn&o3FTmDLWdom*a6820Jg zed`+wmoBN>tH}ax6Qg`3OBtDh8c5q#4f#Ud3|AQV|j6D SGqS@yzjhhsItH@r=l?f2e3H@t literal 0 HcmV?d00001 diff --git a/shelled/rustdesk-as-ref/flutter/assets/more.ttf b/shelled/rustdesk-as-ref/flutter/assets/more.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3b01435df3ac47b3db64ea4196935c064dae987b GIT binary patch literal 1620 zcmd^9Jx>%t7=C7VIc^U{0*N9T*$_k{&Y;3J8G+J1gSlAdFdt+kINCE|xQVKq^I~P&-2fmki=Xsy^W8Rsa8wdbRs6ofT z*u>Ns>)z3ufH*~V^72G~ZurQB&$RE;zMl8&%G%)T_q2a)SZ(PEl_xj(4VZjiTeT-thpD&^UW6|onc^taQteA{zc zo({jHpZQv;5>z9CDZ=flOUDqVhsc0AsV>wUF{>e2gxkfkrT|+(4dpuMXnUj7w~S?Ts7nKfc^zFhW&jS znp{w|#5MTEfxd(ilPdNSS((+xnY$%#pev4HYrv|x#r-FmcCrHq+5i78d*TB)td1c* zD|u41ldmA}d5rW2ylJgKThEBd9Wcd+=oCjpqi7b#YH88hVTvg+k#5iErrx;u$UKzM zvSvqXdh?d3k! zso#Vikv|(ccH|VSnI&sBANa+7hquqOVqQ-BY=-$ ztfL+FIHOCLhs(@xhxgy?X-0>rGEUaCFvy2(U`N&%qx?AX-)^c{;A%Pc7_lRZBV;9y zXjJv*uoIWq;s=zzIo8QDI*W3gRlyMY`K#}z9-oc(>B)yD5&8b@CYsQM4#NvV`G(>* D@=?y^ literal 0 HcmV?d00001 diff --git a/shelled/rustdesk-as-ref/flutter/assets/peer_searchbar.ttf b/shelled/rustdesk-as-ref/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/shelled/rustdesk-as-ref/flutter/assets/tabbar.ttf b/shelled/rustdesk-as-ref/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&2 + return 1 + esac + + echo "*** [$ANDROID_ABI][Start] Build and install vcpkg dependencies" + pushd "$SCRIPTDIR/.." + $VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed" + popd + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true + echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies" + +if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then + echo "*** [Start] Move arm-neon-android to arm-android" + + mv "$VCPKG_ROOT/installed/arm-neon-android" "$VCPKG_ROOT/installed/arm-android" + + echo "*** [Finished] Move arm-neon-android to arm-android" +fi +} + +if [ ! -z "$ANDROID_ABI" ]; then + build "$ANDROID_ABI" +else + echo "Usage: build-android-deps.sh " >&2 + exit 1 +fi diff --git a/shelled/rustdesk-as-ref/flutter/build_fdroid.sh b/shelled/rustdesk-as-ref/flutter/build_fdroid.sh new file mode 100644 index 0000000..d50a6a6 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/build_fdroid.sh @@ -0,0 +1,630 @@ +#!/bin/bash + +# +# Script to build F-Droid release of RustDesk +# +# Copyright (C) 2024, The RustDesk Authors +# 2024, Vasyl Gello +# + +# The script is invoked by F-Droid builder system step-by-step. +# +# It accepts the following arguments: +# +# - versionName from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt +# - versionCode from https://github.com/rustdesk/rustdesk/releases/download/fdroid-version/rustdesk-version.txt +# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64 +# - The build step to execute: +# +# + prebuild: patch sources and do other stuff before the build +# + build: perform actual build of APK file +# + +# Start of functions + +# Install Flutter of version `VERSION` from Github repository +# into directory `FLUTTER_DIR` and apply patches if needed + +prepare_flutter() { + VERSION="${1}" + FLUTTER_DIR="${2}" + + if [ ! -f "${FLUTTER_DIR}/bin/flutter" ]; then + git clone https://github.com/flutter/flutter "${FLUTTER_DIR}" + fi + + pushd "${FLUTTER_DIR}" + + git restore . + git checkout "${VERSION}" + + # Patch flutter + + if dpkg --compare-versions "${VERSION}" ge "3.24.4"; then + git apply "${ROOTDIR}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff" + fi + + flutter config --no-analytics + + popd # ${FLUTTER_DIR} +} + +# Start of script + +set -x + +# Note current working directory as root dir for patches + +ROOTDIR="${PWD}" + +# Parse command-line arguments + +VERNAME="${1}" +VERCODE="${2}" +ANDROID_ABI="${3}" +BUILDSTEP="${4}" + +if [ -z "${VERNAME}" ] || [ -z "${VERCODE}" ] || [ -z "${ANDROID_ABI}" ] || + [ -z "${BUILDSTEP}" ]; then + echo "ERROR: Command-line arguments are all required to be non-empty!" >&2 + exit 1 +fi + +# Set various architecture-specific identifiers + +case "${ANDROID_ABI}" in +arm64-v8a) + FLUTTER_TARGET=android-arm64 + NDK_TARGET=aarch64-linux-android + RUST_TARGET=aarch64-linux-android + RUSTDESK_FEATURES='flutter,hwcodec' + ;; +armeabi-v7a) + FLUTTER_TARGET=android-arm + NDK_TARGET=arm-linux-androideabi + RUST_TARGET=armv7-linux-androideabi + RUSTDESK_FEATURES='flutter,hwcodec' + ;; +x86_64) + FLUTTER_TARGET=android-x64 + NDK_TARGET=x86_64-linux-android + RUST_TARGET=x86_64-linux-android + RUSTDESK_FEATURES='flutter' + ;; +x86) + FLUTTER_TARGET=android-x86 + NDK_TARGET=i686-linux-android + RUST_TARGET=i686-linux-android + RUSTDESK_FEATURES='flutter' + ;; +*) + echo "ERROR: Unknown Android ABI '${ANDROID_ABI}'!" >&2 + exit 1 + ;; +esac + +# Check ANDROID_SDK_ROOT and sdkmanager present on PATH + +if [ ! -d "${ANDROID_SDK_ROOT}" ] || ! command -v sdkmanager 1>/dev/null; then + echo "ERROR: Can not find Android SDK!" >&2 + exit 1 +fi + +# Export necessary variables + +export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools" + +export VCPKG_ROOT="${HOME}/vcpkg" + +# Now act depending on build step + +# NOTE: F-Droid maintainers require explicit declaration of dependencies +# as root via `Builds.sudo` F-Droid metadata directive: +# https://gitlab.com/fdroid/fdroiddata/-/merge_requests/15343#note_1988918695 + +case "${BUILDSTEP}" in +prebuild) + # prebuild: patch sources and do other stuff before the build + + # + # Extract required versions for NDK, Rust, Flutter from + # '.github/workflows/flutter-build.yml' + # + + CARGO_NDK_VERSION="$(yq -r \ + .env.CARGO_NDK_VERSION \ + .github/workflows/flutter-build.yml)" + + # Flutter used to compile main Rustdesk library + + FLUTTER_VERSION="$(yq -r \ + .env.ANDROID_FLUTTER_VERSION \ + .github/workflows/flutter-build.yml)" + + if [ -z "${FLUTTER_VERSION}" ]; then + FLUTTER_VERSION="$(yq -r \ + .env.FLUTTER_VERSION \ + .github/workflows/flutter-build.yml)" + fi + + # Flutter used to compile Flutter<->Rust bridge files + + CARGO_EXPAND_VERSION="$(yq -r \ + .env.CARGO_EXPAND_VERSION \ + .github/workflows/bridge.yml)" + + FLUTTER_BRIDGE_VERSION="$(yq -r \ + .env.FLUTTER_VERSION \ + .github/workflows/bridge.yml)" + + FLUTTER_RUST_BRIDGE_VERSION="$(yq -r \ + .env.FLUTTER_RUST_BRIDGE_VERSION \ + .github/workflows/bridge.yml)" + + NDK_VERSION="$(yq -r \ + .env.NDK_VERSION \ + .github/workflows/flutter-build.yml)" + + RUST_VERSION="$(yq -r \ + .env.RUST_VERSION \ + .github/workflows/flutter-build.yml)" + + VCPKG_COMMIT_ID="$(yq -r \ + .env.VCPKG_COMMIT_ID \ + .github/workflows/flutter-build.yml)" + + if [ -z "${CARGO_NDK_VERSION}" ] || [ -z "${FLUTTER_VERSION}" ] || + [ -z "${FLUTTER_BRIDGE_VERSION}" ] || + [ -z "${FLUTTER_RUST_BRIDGE_VERSION}" ] || + [ -z "${NDK_VERSION}" ] || [ -z "${RUST_VERSION}" ] || + [ -z "${VCPKG_COMMIT_ID}" ]; then + echo "ERROR: Can not identify all required versions!" >&2 + exit 1 + fi + + # Map NDK version to revision + NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json | + jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" | + sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')" + + if [ -z "${NDK_VERSION}" ]; then + echo "ERROR: Can not map Android NDK codename to revision!" >&2 + exit 1 + fi + + export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}" + export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}" + + # + # Install the components + # + + set -e + + # Install Android NDK + + if [ ! -d "${ANDROID_NDK_ROOT}" ]; then + sdkmanager --install "ndk;${NDK_VERSION}" + fi + + # Install Rust + + if [ ! -f "${HOME}/rustup/rustup-init.sh" ]; then + pushd "${HOME}" + + git clone --depth 1 https://github.com/rust-lang/rustup + + popd # ${HOME} + fi + + pushd "${HOME}/rustup" + bash rustup-init.sh -y \ + --target "${RUST_TARGET}" \ + --default-toolchain "${RUST_VERSION}" + popd + + if ! command -v cargo 1>/dev/null 2>&1; then + . "${HOME}/.cargo/env" + fi + + # Install cargo-ndk + + cargo install \ + cargo-ndk \ + --version "${CARGO_NDK_VERSION}" \ + --locked + + # Install rust bridge generator + + cargo install \ + cargo-expand \ + --version "${CARGO_EXPAND_VERSION}" \ + --locked + cargo install flutter_rust_bridge_codegen \ + --version "${FLUTTER_RUST_BRIDGE_VERSION}" \ + --features "uuid" \ + --locked + + # Populate native vcpkg dependencies + + if [ ! -d "${VCPKG_ROOT}" ]; then + pushd "${HOME}" + + git clone \ + https://github.com/Microsoft/vcpkg.git + git clone \ + https://github.com/Microsoft/vcpkg-tool.git + + pushd vcpkg-tool + + mkdir build + + pushd build + + cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -G 'Ninja' \ + -DVCPKG_DEVELOPMENT_WARNINGS=OFF \ + .. + + cmake --build . + + popd # build + + popd # vcpkg-tool + + pushd vcpkg + + git reset --hard "${VCPKG_COMMIT_ID}" + + cp -a ../vcpkg-tool/build/vcpkg vcpkg + + # disable telemetry + + touch "vcpkg.disable-metrics" + + popd # vcpkg + + popd # ${HOME} + fi + + # Install depot-tools for x86 + + if [ "${ANDROID_ABI}" = "x86" ]; then + if [ ! -d "${HOME}/depot_tools" ]; then + pushd "${HOME}" + + git clone \ + --depth 1 \ + https://chromium.googlesource.com/chromium/tools/depot_tools.git + + popd # ${HOME} + fi + fi + + # Patch the RustDesk sources + + git apply res/fdroid/patches/*.patch + + # If Flutter version used to generate bridge files differs from Flutter + # version used to compile Rustdesk library, generate bridge using the + # `FLUTTER_BRIDGE_VERSION` an restore the pubspec later + + if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then + # Find first libclang.so and set BRIDGE_LLVM_PATH + + BRIDGE_LLVM_PATH="$(find /usr/lib/ -name libclang.so | head -n1)" + + if [ -z "${BRIDGE_LLVM_PATH}" ]; then + echo 'ERROR: Can not find libclang.so for bridge generator!' >&2 + exit 1 + fi + + BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")" + BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")" + + # Install Flutter bridge version + + prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter" + + # Save changes + + git add . + + # Edit pubspec to make flutter bridge version work + + sed \ + -i \ + -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \ + flutter/pubspec.yaml + + # Download Flutter dependencies + + pushd flutter + + flutter clean + flutter packages pub get + + popd # flutter + + # Generate FFI bindings + + flutter_rust_bridge_codegen \ + --rust-input ./src/flutter_ffi.rs \ + --dart-output ./flutter/lib/generated_bridge.dart \ + --llvm-path "${BRIDGE_LLVM_PATH}" + + # Add bridge files to save-list + + git add -f ./flutter/lib/generated_bridge.* ./src/bridge_generated.* + + # Restore everything + + git checkout '*' + git clean -dffx + git reset + + unset BRIDGE_LLVM_PATH + fi + + # Install Flutter version for RustDesk library build + + prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter" + + # gms is not in these files now, but we still keep the following line for future reference(maybe). + + sed \ + -i \ + -e '/gms/d' \ + flutter/android/build.gradle \ + flutter/android/app/build.gradle + + # `firebase_analytics` is not in these files now, but we still keep the following lines. + + sed \ + -i \ + -e '/firebase_analytics/d' \ + flutter/pubspec.yaml + + sed \ + -i \ + -e '/ firebase/,/ version/d' \ + flutter/pubspec.lock + + sed \ + -i \ + -e '/firebase/Id' \ + flutter/lib/main.dart + + ;; +build) + # build: perform actual build of APK file + + set -e + + # + # Extract required versions for NDK, Rust, Flutter from + # '.github/workflows/flutter-build.yml' + # + + # Flutter used to compile main Rustdesk library + + FLUTTER_VERSION="$(yq -r \ + .env.ANDROID_FLUTTER_VERSION \ + .github/workflows/flutter-build.yml)" + + if [ -z "${FLUTTER_VERSION}" ]; then + FLUTTER_VERSION="$(yq -r \ + .env.FLUTTER_VERSION \ + .github/workflows/flutter-build.yml)" + fi + + NDK_VERSION="$(yq -r \ + .env.NDK_VERSION \ + .github/workflows/flutter-build.yml)" + + # Map NDK version to revision + NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json | + jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" | + sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')" + + if [ -z "${NDK_VERSION}" ]; then + echo "ERROR: Can not map Android NDK codename to revision!" >&2 + exit 1 + fi + + export ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}" + export ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}" + + if ! command -v cargo 1>/dev/null 2>&1; then + . "${HOME}/.cargo/env" + fi + + # Download Flutter dependencies + + pushd flutter + + flutter clean + flutter packages pub get + + popd # flutter + + # Build host android deps + + bash flutter/build_android_deps.sh "${ANDROID_ABI}" + + # Build rustdesk lib + + cargo ndk \ + --platform 21 \ + --target "${RUST_TARGET}" \ + --bindgen \ + build \ + --release \ + --features "${RUSTDESK_FEATURES}" + + mkdir -p "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}" + + cp "target/${RUST_TARGET}/release/liblibrustdesk.so" \ + "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/librustdesk.so" + + cp "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/${NDK_TARGET}/libc++_shared.so" \ + "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}/" + + "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" \ + "flutter/android/app/src/main/jniLibs/${ANDROID_ABI}"/* + + # Build flutter-jit-release for x86 + + if [ "${ANDROID_ABI}" = "x86" ]; then + pushd flutter-sdk + + echo "## Sync flutter engine sources" + echo "### We need fakeroot because chromium base image is unpacked with weird uid/gid ownership" + + sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" .gclient + + export FAKEROOTDONTTRYCHOWN=1 + + fakeroot gclient sync + + unset FAKEROOTDONTTRYCHOWN + + pushd src + + echo "## Patch away Google Play dependencies" + + rm \ + flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java \ + flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java flutter/shell/platform/android/io/flutter/embedding/android/FlutterPlayStoreSplitApplication.java + + sed \ + -i \ + -e '/PlayStore/d' \ + flutter/tools/android_lint/project.xml \ + flutter/shell/platform/android/BUILD.gn + + sed \ + -i \ + -e '/com.google.android.play/d' \ + flutter/tools/androidx/files.json + + echo "## Configure android engine build" + + flutter/tools/gn \ + --android --android-cpu x86 --runtime-mode=jit_release \ + --no-goma --no-enable-unittests + + echo "## Perform android engine build" + + ninja -C out/android_jit_release_x86 + + echo "## Configure host engine build" + + flutter/tools/gn \ + --android-cpu x86 --runtime-mode=jit_release \ + --no-goma --no-enable-unittests + + echo "## Perform android engine build" + + ninja -C out/host_jit_release_x86 + + echo "## Rename host engine" + + mv out/host_jit_release_x86 out/host_jit_release + + echo "## Mimic jit_release engine to debug to use with flutter build apk" + + pushd out/android_jit_release_x86 + + sed \ + -e 's/jit_release/debug/' \ + flutter_embedding_jit_release.maven-metadata.xml \ + 1>flutter_embedding_debug.maven-metadata.xml + + sed \ + -e 's/jit_release/debug/' \ + flutter_embedding_jit_release.pom \ + 1>flutter_embedding_debug.pom + + sed \ + -e 's/jit_release/debug/' \ + x86_jit_release.maven-metadata.xml \ + 1>x86_debug.maven-metadata.xml + + sed \ + -e 's/jit_release/debug/' \ + x86_jit_release.pom \ + 1>x86_debug.pom + + cp -a \ + flutter_embedding_jit_release-sources.jar \ + flutter_embedding_debug-sources.jar + + cp -a \ + flutter_embedding_jit_release.jar \ + flutter_embedding_debug.jar + + cp -a \ + x86_jit_release.jar \ + x86_debug.jar + + popd # out/android_jit_release_x86 + + popd # src + + popd # flutter-sdk + + echo "# Clean up intermediate engine files and show free space" + + rm -rf \ + flutter-sdk/src/out/android_jit_release_x86/obj \ + flutter-sdk/src/out/host_jit_release/obj + + mv flutter-sdk/src/out flutter-out + + rm -rf flutter-sdk + + mkdir -p flutter-sdk/src/ + + mv flutter-out flutter-sdk/src/out + fi + + # Build the apk + + pushd flutter + + if [ "${ANDROID_ABI}" = "x86" ]; then + flutter build apk \ + --local-engine-src-path="$(readlink -mf "../flutter-sdk/src")" \ + --local-engine=android_jit_release_x86 \ + --debug \ + --build-number="${VERCODE}" \ + --build-name="${VERNAME}" \ + --target-platform "${FLUTTER_TARGET}" + else + flutter build apk \ + --release \ + --build-number="${VERCODE}" \ + --build-name="${VERNAME}" \ + --target-platform "${FLUTTER_TARGET}" + fi + + popd # flutter + + rm -rf flutter-sdk + + # Special step for fdroiddata CI builds to remove .gitconfig + + rm -f /home/vagrant/.gitconfig + + ;; +*) + echo "ERROR: Unknown build step '${BUILDSTEP}'!" >&2 + exit 1 + ;; +esac + +# Report success + +echo "All done!" diff --git a/shelled/rustdesk-as-ref/flutter/build_ios.sh b/shelled/rustdesk-as-ref/flutter/build_ios.sh new file mode 100644 index 0000000..cd12626 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/build_ios.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# https://docs.flutter.dev/deployment/ios +# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info +# no obfuscate, because no easy to check errors +cd $(dirname $(dirname $(which flutter))) +git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff +cd - +flutter build ipa --release diff --git a/shelled/rustdesk-as-ref/flutter/ios/.gitignore b/shelled/rustdesk-as-ref/flutter/ios/.gitignore new file mode 100644 index 0000000..151026b --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/.gitignore @@ -0,0 +1,33 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/shelled/rustdesk-as-ref/flutter/ios/Flutter/AppFrameworkInfo.plist b/shelled/rustdesk-as-ref/flutter/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Flutter/Debug.xcconfig b/shelled/rustdesk-as-ref/flutter/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/shelled/rustdesk-as-ref/flutter/ios/Flutter/Release.xcconfig b/shelled/rustdesk-as-ref/flutter/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/shelled/rustdesk-as-ref/flutter/ios/Podfile b/shelled/rustdesk-as-ref/flutter/ios/Podfile new file mode 100644 index 0000000..b71c436 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Podfile @@ -0,0 +1,45 @@ +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +platform :ios, '13.0' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', '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 Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' + end + end +end + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Podfile.lock b/shelled/rustdesk-as-ref/flutter/ios/Podfile.lock new file mode 100644 index 0000000..c9e9f9a --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Podfile.lock @@ -0,0 +1,142 @@ +PODS: + - device_info_plus (0.0.1): + - Flutter + - DKImagePickerController/Core (4.3.4): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.4) + - DKImagePickerController/PhotoGallery (4.3.4): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.4) + - DKPhotoGallery (0.0.17): + - DKPhotoGallery/Core (= 0.0.17) + - DKPhotoGallery/Model (= 0.0.17) + - DKPhotoGallery/Preview (= 0.0.17) + - DKPhotoGallery/Resource (= 0.0.17) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.17): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.17): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Flutter (1.0.0) + - flutter_keyboard_visibility (0.0.1): + - Flutter + - image_picker_ios (0.0.1): + - Flutter + - MTBBarcodeScanner (5.0.11) + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - qr_code_scanner (0.2.0): + - Flutter + - MTBBarcodeScanner + - SDWebImage (5.18.11): + - SDWebImage/Core (= 5.18.11) + - SDWebImage/Core (5.18.11) + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + - SwiftyGif (5.4.4) + - uni_links (0.0.1): + - Flutter + - url_launcher_ios (0.0.1): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + - wakelock_plus (0.0.1): + - Flutter + +DEPENDENCIES: + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - uni_links (from `.symlinks/plugins/uni_links/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - MTBBarcodeScanner + - SDWebImage + - SwiftyGif + +EXTERNAL SOURCES: + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + qr_code_scanner: + :path: ".symlinks/plugins/qr_code_scanner/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" + uni_links: + :path: ".symlinks/plugins/uni_links/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" + +SPEC CHECKSUMS: + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac + DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 + MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e + SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + uni_links: d97da20c7701486ba192624d99bffaaffcfc298a + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 + wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 + +PODFILE CHECKSUM: 83d1b0fb6fc8613d8312a03b8e1540d37cfc5d2c + +COCOAPODS: 1.15.2 diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.pbxproj b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..36dc89e --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,756 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + C5678348E08E565F424B13A5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25E069BDBEF2890938537ABB /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0A3A301029F021DD0095DDA5 /* liblibrustdesk.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = liblibrustdesk.a; path = "../../target/aarch64-apple-ios/release/liblibrustdesk.a"; sourceTree = ""; }; + 0A3A301329F0AB660095DDA5 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 25E069BDBEF2890938537ABB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 40E78CC6B4293A3E6DA85154 /* 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 = ""; }; + 4151ACC476A12FDC49BC7860 /* 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C66E40D850585BD073A0EF7D /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C5678348E08E565F424B13A5 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + FC03317C0FF27D4E19FBA24E /* Pods */, + F213315C743C5EC601AD8123 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 0A3A301329F0AB660095DDA5 /* Runner.entitlements */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + F213315C743C5EC601AD8123 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0A3A301029F021DD0095DDA5 /* liblibrustdesk.a */, + 25E069BDBEF2890938537ABB /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + FC03317C0FF27D4E19FBA24E /* Pods */ = { + isa = PBXGroup; + children = ( + 40E78CC6B4293A3E6DA85154 /* Pods-Runner.debug.xcconfig */, + C66E40D850585BD073A0EF7D /* Pods-Runner.release.xcconfig */, + 4151ACC476A12FDC49BC7860 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 7EB62F3CF0939A56238651D9 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 649B29BA4C122652F1215682 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 649B29BA4C122652F1215682 /* [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; + }; + 7EB62F3CF0939A56238651D9 /* [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; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = HZF9JMC8YN; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + /usr/lib/swift, + "$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/", + "$(SDKROOT)/usr/lib/swift", + "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/liblibrustdesk.a", + "-ObjC", + "-l\"c++\"", + "-l\"sqlite3\"", + "-framework", + "\"AVFoundation\"", + "-framework", + "\"AVKit\"", + "-framework", + "\"DKImagePickerController\"", + "-framework", + "\"DKPhotoGallery\"", + "-framework", + "\"Foundation\"", + "-framework", + "\"ImageIO\"", + "-framework", + "\"MTBBarcodeScanner\"", + "-framework", + "\"Photos\"", + "-framework", + "\"QuartzCore\"", + "-framework", + "\"SDWebImage\"", + "-framework", + "\"SwiftyGif\"", + "-framework", + "\"UIKit\"", + "-framework", + "\"device_info_plus\"", + "-framework", + "\"file_picker\"", + "-framework", + "\"flutter_keyboard_visibility\"", + "-framework", + "\"image_picker_ios\"", + "-framework", + "\"package_info_plus\"", + "-framework", + "\"path_provider_foundation\"", + "-framework", + "\"qr_code_scanner\"", + "-framework", + "\"sqflite\"", + "-framework", + "\"uni_links\"", + "-framework", + "\"url_launcher_ios\"", + "-framework", + "\"video_player_avfoundation\"", + "-framework", + "\"wakelock_plus\"", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_STYLE = "non-global"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + 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_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + 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_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = HZF9JMC8YN; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + /usr/lib/swift, + "$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/", + "$(SDKROOT)/usr/lib/swift", + "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/liblibrustdesk.a", + "-ObjC", + "-l\"c++\"", + "-l\"sqlite3\"", + "-framework", + "\"AVFoundation\"", + "-framework", + "\"AVKit\"", + "-framework", + "\"DKImagePickerController\"", + "-framework", + "\"DKPhotoGallery\"", + "-framework", + "\"Foundation\"", + "-framework", + "\"ImageIO\"", + "-framework", + "\"MTBBarcodeScanner\"", + "-framework", + "\"Photos\"", + "-framework", + "\"QuartzCore\"", + "-framework", + "\"SDWebImage\"", + "-framework", + "\"SwiftyGif\"", + "-framework", + "\"UIKit\"", + "-framework", + "\"device_info_plus\"", + "-framework", + "\"file_picker\"", + "-framework", + "\"flutter_keyboard_visibility\"", + "-framework", + "\"image_picker_ios\"", + "-framework", + "\"package_info_plus\"", + "-framework", + "\"path_provider_foundation\"", + "-framework", + "\"qr_code_scanner\"", + "-framework", + "\"sqflite\"", + "-framework", + "\"uni_links\"", + "-framework", + "\"url_launcher_ios\"", + "-framework", + "\"video_player_avfoundation\"", + "-framework", + "\"wakelock_plus\"", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_STYLE = "non-global"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = HZF9JMC8YN; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + /usr/lib/swift, + "$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/", + "$(SDKROOT)/usr/lib/swift", + "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/liblibrustdesk.a", + "-ObjC", + "-l\"c++\"", + "-l\"sqlite3\"", + "-framework", + "\"AVFoundation\"", + "-framework", + "\"AVKit\"", + "-framework", + "\"DKImagePickerController\"", + "-framework", + "\"DKPhotoGallery\"", + "-framework", + "\"Foundation\"", + "-framework", + "\"ImageIO\"", + "-framework", + "\"MTBBarcodeScanner\"", + "-framework", + "\"Photos\"", + "-framework", + "\"QuartzCore\"", + "-framework", + "\"SDWebImage\"", + "-framework", + "\"SwiftyGif\"", + "-framework", + "\"UIKit\"", + "-framework", + "\"device_info_plus\"", + "-framework", + "\"file_picker\"", + "-framework", + "\"flutter_keyboard_visibility\"", + "-framework", + "\"image_picker_ios\"", + "-framework", + "\"package_info_plus\"", + "-framework", + "\"path_provider_foundation\"", + "-framework", + "\"qr_code_scanner\"", + "-framework", + "\"sqflite\"", + "-framework", + "\"uni_links\"", + "-framework", + "\"url_launcher_ios\"", + "-framework", + "\"video_player_avfoundation\"", + "-framework", + "\"wakelock_plus\"", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_STYLE = "non-global"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..5e31d3d --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner/AppDelegate.swift b/shelled/rustdesk-as-ref/flutter/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..d9333b7 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner/AppDelegate.swift @@ -0,0 +1,19 @@ +import UIKit +import Flutter + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + dummyMethodToEnforceBundling(); + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + public func dummyMethodToEnforceBundling() { + dummy_method_to_enforce_bundling(); + session_get_rgba(nil, 0); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..5361129 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-60x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-60x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-20x20@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-76x76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-76x76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-83.5x83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "Icon-App-1024x1024@1x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..00cabce --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "LaunchImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "LaunchImage@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "LaunchImage@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard b/shelled/rustdesk-as-ref/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner/Base.lproj/Main.storyboard b/shelled/rustdesk-as-ref/flutter/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..d68a3a7 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner/GoogleService-Info.plist b/shelled/rustdesk-as-ref/flutter/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..f392882 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 768133699366-k1rn3ls1u2n3nklmgd9t4cmpdob0c8bn.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.768133699366-k1rn3ls1u2n3nklmgd9t4cmpdob0c8bn + API_KEY + AIzaSyCf57HjCwSokt91CqFI0Mwf8D--ek0jvfc + GCM_SENDER_ID + 768133699366 + PLIST_VERSION + 1 + BUNDLE_ID + com.carriez.flutterHbb + PROJECT_ID + rustdesk + STORAGE_BUCKET + rustdesk.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:768133699366:ios:c33078a6181b9d507993e7 + DATABASE_URL + https://rustdesk.firebaseio.com + + \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner/Info.plist b/shelled/rustdesk-as-ref/flutter/ios/Runner/Info.plist new file mode 100644 index 0000000..9351dac --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner/Info.plist @@ -0,0 +1,82 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + RustDesk + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + RustDesk + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLIconFile + + CFBundleURLName + com.carriez.rustdesk + CFBundleURLSchemes + + rustdesk + + + + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UIFileSharingEnabled + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportsDocumentBrowser + + UIViewControllerBasedStatusBarAppearance + + ITSAppUsesNonExemptEncryption + + io.flutter.embedded_views_preview + + NSCameraUsageDescription + This app needs camera access to scan QR codes + NSPhotoLibraryUsageDescription + This app needs photo library access to get QR codes from image + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner/Runner-Bridging-Header.h b/shelled/rustdesk-as-ref/flutter/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..e930a39 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1,3 @@ +#import "GeneratedPluginRegistrant.h" + +#import "bridge_generated.h" diff --git a/shelled/rustdesk-as-ref/flutter/ios/Runner/Runner.entitlements b/shelled/rustdesk-as-ref/flutter/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..75e36a1 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + aps-environment + development + com.apple.developer.networking.wifi-info + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios/exportOptions.plist b/shelled/rustdesk-as-ref/flutter/ios/exportOptions.plist new file mode 100644 index 0000000..6ceb2ac --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios/exportOptions.plist @@ -0,0 +1,15 @@ + + + + + method + app-store + teamID + HZF9JMC8YN + provisioningProfiles + + com.carriez.flutterHbb + rustdesk-ios-prod-app-store + + + diff --git a/shelled/rustdesk-as-ref/flutter/ios_arm64.sh b/shelled/rustdesk-as-ref/flutter/ios_arm64.sh new file mode 100644 index 0000000..579baaa --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios_arm64.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib diff --git a/shelled/rustdesk-as-ref/flutter/ios_x64.sh b/shelled/rustdesk-as-ref/flutter/ios_x64.sh new file mode 100644 index 0000000..04b9993 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/ios_x64.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cargo build --features flutter --release --target x86_64-apple-ios --lib diff --git a/shelled/rustdesk-as-ref/flutter/lib/common.dart b/shelled/rustdesk-as-ref/flutter/lib/common.dart new file mode 100644 index 0000000..ad3bbc9 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common.dart @@ -0,0 +1,4161 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:back_button_interceptor/back_button_interceptor.dart'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/formatter/id_formatter.dart'; +import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/main.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; +import 'package:provider/provider.dart'; +import 'package:uni_links/uni_links.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:uuid/uuid.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:window_manager/window_manager.dart'; +import 'package:window_size/window_size.dart' as window_size; + +import '../consts.dart'; +import 'common/widgets/overlay.dart'; +import 'mobile/pages/file_manager_page.dart'; +import 'mobile/pages/remote_page.dart'; +import 'mobile/pages/view_camera_page.dart'; +import 'mobile/pages/terminal_page.dart'; +import 'desktop/pages/remote_page.dart' as desktop_remote; +import 'desktop/pages/file_manager_page.dart' as desktop_file_manager; +import 'desktop/pages/view_camera_page.dart' as desktop_view_camera; +import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; +import 'models/model.dart'; +import 'models/platform_model.dart'; + +import 'package:flutter_hbb/native/win32.dart' + if (dart.library.html) 'package:flutter_hbb/web/win32.dart'; +import 'package:flutter_hbb/native/common.dart' + if (dart.library.html) 'package:flutter_hbb/web/common.dart'; +import 'package:flutter_hbb/utils/http_service.dart' as http; + +final globalKey = GlobalKey(); +final navigationBarKey = GlobalKey(); + +final isAndroid = isAndroid_; +final isIOS = isIOS_; +final isWindows = isWindows_; +final isMacOS = isMacOS_; +final isLinux = isLinux_; +final isDesktop = isDesktop_; +final isWeb = isWeb_; +final isWebDesktop = isWebDesktop_; +final isWebOnWindows = isWebOnWindows_; +final isWebOnLinux = isWebOnLinux_; +final isWebOnMacOs = isWebOnMacOS_; +var isMobile = isAndroid || isIOS; +var version = ''; +int androidVersion = 0; + +// Only used on Linux. +// `windowManager.setResizable(false)` will reset the window size to the default size on Linux. +// https://stackoverflow.com/questions/8193613/gtk-window-resize-disable-without-going-back-to-default +// So we need to use this flag to enable/disable resizable. +bool _linuxWindowResizable = true; + +// Only used on Windows(window manager). +bool _ignoreDevicePixelRatio = true; + +/// only available for Windows target +int windowsBuildNumber = 0; +DesktopType? desktopType; + +// Tolerance used for floating-point position comparisons to avoid precision errors. +const double _kPositionEpsilon = 1e-6; + +bool get isMainDesktopWindow => + desktopType == DesktopType.main || desktopType == DesktopType.cm; + +String get screenInfo => screenInfo_; + +/// Check if the app is running with single view mode. +bool isSingleViewApp() { + return desktopType == DesktopType.cm; +} + +/// * debug or test only, DO NOT enable in release build +bool isTest = false; + +typedef F = String Function(String); +typedef FMethod = String Function(String, dynamic); + +typedef StreamEventHandler = Future Function(Map); +typedef SessionID = UuidValue; +final iconHardDrive = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAmVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjHWqVAAAAMnRSTlMAv0BmzLJNXlhiUu2fxXDgu7WuSUUe29LJvpqUjX53VTstD7ilNujCqTEk5IYH+vEoFjKvAagAAAPpSURBVHja7d0JbhpBEIXhB3jYzb5vBgzYgO04df/DJXGUKMwU9ECmZ6pQfSfw028LCXW3YYwxxhhjjDHGGGOM0eZ9VV1MckdKWLM1bRQ/35GW/WxHHu1me6ShuyHvNl34VhlTKsYVeDWj1EzgUZ1S1DrAk/UDparZgxd9Sl0BHnxSBhpI3jfKQG2FpLUpE69I2ILikv1nsvygjBwPSNKYMlNHggqUoSKS80AZCnwHqQ1zCRvW+CRegwRFeFAMKKrtM8gTPJlzSfwFgT9dJom3IDN4VGaSeAryAK8m0SSeghTg1ZYiql6CjBDhO8mzlyAVhKhIwgXxrh5NojGIhyRckEdwpCdhgpSQgiWTRGMQNonGIGySp0SDvMDBX5KWxiB8Eo1BgE00SYJBykhNnkmSWJAcLpGaJNMgfJKyxiDAK4WNEwryhMtkJsk8CJtEYxA+icYgQIfCcgkEqcJNXhIRQdgkGoPwSTQG+e8khdu/7JOVREwQIKCwF41B2CQljUH4JLcH6SI+OUlEBQHa0SQag/BJNAbhkjxqDMIn0RgEeI4muSlID9eSkERgEKAVTaIxCJ9EYxA2ydVB8hCASVLRGAQYR5NoDMIn0RgEyFHYSGMQPonGII4kziCNvBgNJonEk4u3GAk8Sprk6eYaqbMDY0oKvUm5jfC/viGiSypV7+M3i2iDsAGpNEDYjlTa3W8RdR/r544g50ilnA0RxoZIE2NIXqQbhkAkGyKNDZHGhkhjQ6SxIdLYEGlsiDQ2JGTVeD0264U9zipPh7XOooffpA6pfNCXjxl4/c3pUzlChwzor53zwYYVfpI5pOV6LWFF/2jiJ5FDSs5jdY/0rwUAkUMeXWdBqnSqD0DikBqdqCHsjTvELm9In0IOri/0pwAEDtlSyNaRjAIAAoesKWTtuusxByBwCJp0oomwBXcYUuCQgE50ENajE4OvZAKHLB1/68Br5NqiyCGYOY8YRd77kTkEb64n7lZN+mOIX4QOwb5FX0ZVx3uOxwW+SB0CbBubemWP8/rlaaeRX+M3uUOuZENsiA25zIbYkPsZElBIHwL13U/PTjJ/cyOOEoVM3I+hziDQlELm7pPxw3eI8/7gPh1fpLA6xGnEeDDgO0UcIAzzM35HxLPIq5SXe9BLzOsj9eUaQqyXzxS1QFSfWM2cCANiHcAISJ0AnCKpUwTuIkkA3EeSInAXSQKcs1V18e24wlllUmQp9v9zXKeHi+akRAMOPVKhAqdPBZeUmnnEsO6QcJ0+4qmOSbBxFfGVRiTUqITrdKcCbyYO3/K4wX4+aQ+FfNjXhu3JfAVjjDHGGGOMMcYYY4xIPwCgfqT6TbhCLAAAAABJRU5ErkJggg=='))); + +enum DesktopType { + main, + remote, + fileTransfer, + viewCamera, + terminal, + cm, + portForward, +} + +bool isDoubleEqual(double a, double b) { + return (a - b).abs() < _kPositionEpsilon; +} + +class IconFont { + static const _family1 = 'Tabbar'; + static const _family2 = 'PeerSearchbar'; + static const _family3 = 'AddressBook'; + static const _family4 = 'DeviceGroup'; + static const _family5 = 'More'; + + IconFont._(); + + 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 roundClose = IconData(0xe6ed, fontFamily: _family2); + static const IconData addressBook = IconData(0xe602, fontFamily: _family3); + static const IconData deviceGroupOutline = + IconData(0xe623, fontFamily: _family4); + static const IconData deviceGroupFill = + IconData(0xe748, fontFamily: _family4); + static const IconData more = IconData(0xe609, fontFamily: _family5); +} + +class ColorThemeExtension extends ThemeExtension { + const ColorThemeExtension({ + required this.border, + required this.border2, + required this.border3, + required this.highlight, + required this.drag_indicator, + required this.shadow, + required this.errorBannerBg, + required this.me, + required this.toastBg, + required this.toastText, + required this.divider, + }); + + final Color? border; + final Color? border2; + final Color? border3; + final Color? highlight; + final Color? drag_indicator; + final Color? shadow; + final Color? errorBannerBg; + final Color? me; + final Color? toastBg; + final Color? toastText; + final Color? divider; + + static final light = ColorThemeExtension( + border: Color(0xFFCCCCCC), + border2: Color(0xFFBBBBBB), + border3: Colors.black26, + highlight: Color(0xFFE5E5E5), + drag_indicator: Colors.grey[800], + shadow: Colors.black, + errorBannerBg: Color(0xFFFDEEEB), + me: Colors.green, + toastBg: Colors.black.withOpacity(0.6), + toastText: Colors.white, + divider: Colors.black38, + ); + + static final dark = ColorThemeExtension( + border: Color(0xFF555555), + border2: Color(0xFFE5E5E5), + border3: Colors.white24, + highlight: Color(0xFF3F3F3F), + drag_indicator: Colors.grey, + shadow: Colors.grey, + errorBannerBg: Color(0xFF470F2D), + me: Colors.greenAccent, + toastBg: Colors.white.withOpacity(0.6), + toastText: Colors.black, + divider: Colors.white38, + ); + + @override + ThemeExtension copyWith({ + Color? border, + Color? border2, + Color? border3, + Color? highlight, + Color? drag_indicator, + Color? shadow, + Color? errorBannerBg, + Color? me, + Color? toastBg, + Color? toastText, + Color? divider, + }) { + return ColorThemeExtension( + border: border ?? this.border, + border2: border2 ?? this.border2, + border3: border3 ?? this.border3, + highlight: highlight ?? this.highlight, + drag_indicator: drag_indicator ?? this.drag_indicator, + shadow: shadow ?? this.shadow, + errorBannerBg: errorBannerBg ?? this.errorBannerBg, + me: me ?? this.me, + toastBg: toastBg ?? this.toastBg, + toastText: toastText ?? this.toastText, + divider: divider ?? this.divider, + ); + } + + @override + ThemeExtension lerp( + ThemeExtension? other, double t) { + if (other is! ColorThemeExtension) { + return this; + } + return ColorThemeExtension( + border: Color.lerp(border, other.border, t), + border2: Color.lerp(border2, other.border2, t), + border3: Color.lerp(border3, other.border3, t), + highlight: Color.lerp(highlight, other.highlight, t), + drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t), + shadow: Color.lerp(shadow, other.shadow, t), + errorBannerBg: Color.lerp(shadow, other.errorBannerBg, t), + me: Color.lerp(shadow, other.me, t), + toastBg: Color.lerp(shadow, other.toastBg, t), + toastText: Color.lerp(shadow, other.toastText, t), + divider: Color.lerp(shadow, other.divider, t), + ); + } +} + +class MyTheme { + MyTheme._(); + + static const Color grayBg = Color(0xFFEFEFF2); + static const Color accent = Color(0xFF0071FF); + static const Color accent50 = Color(0x770071FF); + static const Color accent80 = Color(0xAA0071FF); + static const Color canvasColor = Color(0xFF212121); + static const Color border = Color(0xFFCCCCCC); + static const Color idColor = Color(0xFF00B6F0); + static const Color darkGray = Color.fromARGB(255, 148, 148, 148); + static const Color cmIdColor = Color(0xFF21790B); + static const Color dark = Colors.black87; + static const Color button = Color(0xFF2C8CFF); + static const Color hoverBorder = Color(0xFF999999); + + // ListTile + static const ListTileThemeData listTileTheme = ListTileThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ); + + static SwitchThemeData switchTheme() { + return SwitchThemeData( + splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius); + } + + static RadioThemeData radioTheme() { + return RadioThemeData( + splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius); + } + + // Checkbox + static const CheckboxThemeData checkboxTheme = CheckboxThemeData( + splashRadius: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ); + + // TextButton + // Value is used to calculate "dialog.actionsPadding" + static const double mobileTextButtonPaddingLR = 20; + + // TextButton on mobile needs a fixed padding, otherwise small buttons + // like "OK" has a larger left/right padding. + static TextButtonThemeData mobileTextButtonTheme = TextButtonThemeData( + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric(horizontal: mobileTextButtonPaddingLR), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ); + + //tooltip + static TooltipThemeData tooltipTheme() { + return TooltipThemeData( + waitDuration: Duration(seconds: 1, milliseconds: 500), + ); + } + + // Dialogs + static const double dialogPadding = 24; + + // padding bottom depends on content (some dialogs has no content) + static EdgeInsets dialogTitlePadding({bool content = true}) { + final double p = dialogPadding; + + return EdgeInsets.fromLTRB(p, p, p, content ? 0 : p); + } + + // padding bottom depends on actions (mobile has dialogs without actions) + static EdgeInsets dialogContentPadding({bool actions = true}) { + final double p = dialogPadding; + + return (isDesktop || isWebDesktop) + ? EdgeInsets.fromLTRB(p, p, p, actions ? (p - 4) : p) + : EdgeInsets.fromLTRB(p, p, p, actions ? (p / 2) : p); + } + + static EdgeInsets dialogActionsPadding() { + final double p = dialogPadding; + + return (isDesktop || isWebDesktop) + ? EdgeInsets.fromLTRB(p, 0, p, (p - 4)) + : EdgeInsets.fromLTRB(p, 0, (p - mobileTextButtonPaddingLR), (p / 2)); + } + + static EdgeInsets dialogButtonPadding = (isDesktop || isWebDesktop) + ? EdgeInsets.only(left: dialogPadding) + : EdgeInsets.only(left: dialogPadding / 3); + + static ScrollbarThemeData scrollbarTheme = ScrollbarThemeData( + thickness: MaterialStateProperty.all(6), + thumbColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.dragged)) { + return Colors.grey[900]; + } else if (states.contains(MaterialState.hovered)) { + return Colors.grey[700]; + } else { + return Colors.grey[500]; + } + }), + crossAxisMargin: 4, + ); + + static ScrollbarThemeData scrollbarThemeDark = scrollbarTheme.copyWith( + thumbColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.dragged)) { + return Colors.grey[100]; + } else if (states.contains(MaterialState.hovered)) { + return Colors.grey[300]; + } else { + return Colors.grey[500]; + } + }), + ); + + static ThemeData lightTheme = ThemeData( + // https://stackoverflow.com/questions/77537315/after-upgrading-to-flutter-3-16-the-app-bar-background-color-button-size-and + useMaterial3: false, + brightness: Brightness.light, + hoverColor: Color.fromARGB(255, 224, 224, 224), + scaffoldBackgroundColor: Colors.white, + dialogBackgroundColor: Colors.white, + appBarTheme: AppBarTheme( + shadowColor: Colors.transparent, + ), + dialogTheme: DialogTheme( + elevation: 15, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + side: BorderSide( + width: 1, + color: grayBg, + ), + ), + ), + scrollbarTheme: scrollbarTheme, + inputDecorationTheme: isDesktop + ? InputDecorationTheme( + fillColor: grayBg, + filled: true, + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ) + : null, + textTheme: const TextTheme( + titleLarge: TextStyle(fontSize: 19, color: Colors.black87), + titleSmall: TextStyle(fontSize: 14, color: Colors.black87), + bodySmall: TextStyle(fontSize: 12, color: Colors.black87, height: 1.25), + bodyMedium: + TextStyle(fontSize: 14, color: Colors.black87, height: 1.25), + labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)), + cardColor: grayBg, + hintColor: Color(0xFFAAAAAA), + visualDensity: VisualDensity.adaptivePlatformDensity, + tabBarTheme: const TabBarTheme( + labelColor: Colors.black87, + ), + tooltipTheme: tooltipTheme(), + splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null, + highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null, + splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null, + textButtonTheme: (isDesktop || isWebDesktop) + ? TextButtonThemeData( + style: TextButton.styleFrom( + splashFactory: NoSplash.splashFactory, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ) + : mobileTextButtonTheme, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: MyTheme.accent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + backgroundColor: grayBg, + foregroundColor: Colors.black87, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + switchTheme: switchTheme(), + radioTheme: radioTheme(), + checkboxTheme: checkboxTheme, + listTileTheme: listTileTheme, + menuBarTheme: MenuBarThemeData( + style: + MenuStyle(backgroundColor: MaterialStatePropertyAll(Colors.white))), + colorScheme: ColorScheme.light( + primary: Colors.blue, secondary: accent, background: grayBg), + popupMenuTheme: PopupMenuThemeData( + color: Colors.white, + shape: RoundedRectangleBorder( + side: BorderSide( + color: (isDesktop || isWebDesktop) + ? Color(0xFFECECEC) + : Colors.transparent), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + )), + ).copyWith( + extensions: >[ + ColorThemeExtension.light, + TabbarTheme.light, + ], + ); + static ThemeData darkTheme = ThemeData( + useMaterial3: false, + brightness: Brightness.dark, + hoverColor: Color.fromARGB(255, 45, 46, 53), + scaffoldBackgroundColor: Color(0xFF18191E), + dialogBackgroundColor: Color(0xFF18191E), + appBarTheme: AppBarTheme( + shadowColor: Colors.transparent, + ), + dialogTheme: DialogTheme( + elevation: 15, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + side: BorderSide( + width: 1, + color: Color(0xFF24252B), + ), + ), + ), + scrollbarTheme: scrollbarThemeDark, + inputDecorationTheme: (isDesktop || isWebDesktop) + ? InputDecorationTheme( + fillColor: Color(0xFF24252B), + filled: true, + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ) + : null, + textTheme: const TextTheme( + titleLarge: TextStyle(fontSize: 19), + titleSmall: TextStyle(fontSize: 14), + bodySmall: TextStyle(fontSize: 12, height: 1.25), + bodyMedium: TextStyle(fontSize: 14, height: 1.25), + labelLarge: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + color: accent80, + ), + ), + cardColor: Color(0xFF24252B), + visualDensity: VisualDensity.adaptivePlatformDensity, + tabBarTheme: const TabBarTheme( + labelColor: Colors.white70, + ), + tooltipTheme: tooltipTheme(), + splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null, + highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null, + splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null, + textButtonTheme: (isDesktop || isWebDesktop) + ? TextButtonThemeData( + style: TextButton.styleFrom( + splashFactory: NoSplash.splashFactory, + disabledForegroundColor: Colors.white70, + foregroundColor: Colors.white70, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ) + : mobileTextButtonTheme, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: MyTheme.accent, + foregroundColor: Colors.white, + disabledForegroundColor: Colors.white70, + disabledBackgroundColor: Colors.white10, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + backgroundColor: Color(0xFF24252B), + side: BorderSide(color: Colors.white12, width: 0.5), + disabledForegroundColor: Colors.white70, + foregroundColor: Colors.white70, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + switchTheme: switchTheme(), + radioTheme: radioTheme(), + checkboxTheme: checkboxTheme, + listTileTheme: listTileTheme, + menuBarTheme: MenuBarThemeData( + style: MenuStyle( + backgroundColor: MaterialStatePropertyAll(Color(0xFF121212)))), + colorScheme: ColorScheme.dark( + primary: Colors.blue, + secondary: accent, + background: Color(0xFF24252B), + ), + popupMenuTheme: PopupMenuThemeData( + shape: RoundedRectangleBorder( + side: BorderSide(color: Colors.white24), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + )), + ).copyWith( + extensions: >[ + ColorThemeExtension.dark, + TabbarTheme.dark, + ], + ); + + static ThemeMode getThemeModePreference() { + return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme)); + } + + static Future changeDarkMode(ThemeMode mode) async { + Get.changeThemeMode(mode); + if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) { + if (mode == ThemeMode.system) { + await bind.mainSetLocalOption( + key: kCommConfKeyTheme, value: defaultOptionTheme); + } else { + await bind.mainSetLocalOption( + key: kCommConfKeyTheme, value: mode.toShortString()); + } + if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString()); + // Synchronize the window theme of the system. + updateSystemWindowTheme(); + } + } + + static ThemeMode currentThemeMode() { + final preference = getThemeModePreference(); + if (preference == ThemeMode.system) { + if (WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.light) { + return ThemeMode.light; + } else { + return ThemeMode.dark; + } + } else { + return preference; + } + } + + static ColorThemeExtension color(BuildContext context) { + return Theme.of(context).extension()!; + } + + static TabbarTheme tabbar(BuildContext context) { + return Theme.of(context).extension()!; + } + + static ThemeMode themeModeFromString(String v) { + switch (v) { + case "light": + return ThemeMode.light; + case "dark": + return ThemeMode.dark; + default: + return ThemeMode.system; + } + } +} + +extension ParseToString on ThemeMode { + String toShortString() { + return toString().split('.').last; + } +} + +final ButtonStyle flatButtonStyle = TextButton.styleFrom( + minimumSize: Size(0, 36), + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(2.0)), + ), +); + +List supportedLocales = const [ + Locale('en', 'US'), + Locale('zh', 'CN'), + Locale('zh', 'TW'), + Locale('zh', 'SG'), + Locale('fr'), + Locale('de'), + Locale('it'), + Locale('ja'), + Locale('cs'), + Locale('pl'), + Locale('ko'), + Locale('hu'), + Locale('pt'), + Locale('ru'), + Locale('sk'), + Locale('id'), + Locale('da'), + Locale('eo'), + Locale('tr'), + Locale('kz'), + Locale('es'), + Locale('nl'), + Locale('nb'), + Locale('et'), + Locale('eu'), + Locale('bg'), + Locale('be'), + Locale('vn'), + Locale('uk'), + Locale('fa'), + Locale('ca'), + Locale('el'), + Locale('sv'), + Locale('sq'), + Locale('sr'), + Locale('th'), + Locale('sl'), + Locale('ro'), + Locale('lt'), + Locale('lv'), + Locale('ar'), + Locale('he'), + Locale('hr'), +]; + +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) { + () async { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + gFFI.chatModel.hideChatOverlay(); + Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + stateGlobal.isInMainPage = true; + }(); + } else { + if (isWeb) { + Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + stateGlobal.isInMainPage = true; + } else { + final controller = Get.find(); + controller.closeBy(id); + } + } +} + +Future windowOnTop(int? id) async { + if (!isDesktop) { + return; + } + print("Bring window '$id' on top"); + if (id == null) { + // main window + if (stateGlobal.isMinimized) { + await windowManager.restore(); + } + await windowManager.show(); + await windowManager.focus(); + await rustDeskWinManager.registerActiveWindow(kWindowMainId); + } else { + WindowController.fromWindowId(id) + ..focus() + ..show(); + rustDeskWinManager.call(WindowType.Main, kWindowEventShow, {"id": id}); + } +} + +typedef DialogBuilder = CustomAlertDialog Function( + StateSetter setState, void Function([dynamic]) close, BuildContext context); + +class Dialog { + OverlayEntry? entry; + Completer completer = Completer(); + + Dialog(); + + void complete(T? res) { + try { + if (!completer.isCompleted) { + completer.complete(res); + } + } catch (e) { + debugPrint("Dialog complete catch error: $e"); + } finally { + entry?.remove(); + } + } +} + +class OverlayKeyState { + final _overlayKey = GlobalKey(); + + /// use global overlay by default + OverlayState? get state => + _overlayKey.currentState ?? globalKey.currentState?.overlay; + + GlobalKey? get key => _overlayKey; +} + +class OverlayDialogManager { + final Map _dialogs = {}; + var _overlayKeyState = OverlayKeyState(); + int _tagCount = 0; + + OverlayEntry? _mobileActionsOverlayEntry; + RxBool mobileActionsOverlayVisible = true.obs; + + setMobileActionsOverlayVisible(bool v, {store = true}) { + if (store) { + bind.setLocalFlutterOption(k: kOptionShowMobileAction, v: v ? 'Y' : 'N'); + } + // No need to read the value from local storage after setting it. + // It better to toggle the value directly. + mobileActionsOverlayVisible.value = v; + } + + loadMobileActionsOverlayVisible() { + mobileActionsOverlayVisible.value = + bind.getLocalFlutterOption(k: kOptionShowMobileAction) != 'N'; + } + + void setOverlayState(OverlayKeyState overlayKeyState) { + _overlayKeyState = overlayKeyState; + } + + 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); + } + + Future show(DialogBuilder builder, + {bool clickMaskDismiss = false, + bool backDismiss = false, + String? tag, + bool useAnimation = true, + bool forceGlobal = false}) { + final overlayState = + forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state; + + if (overlayState == null) { + return Future.error( + "[OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first"); + } + + final String dialogTag; + if (tag != null) { + dialogTag = tag; + } else { + dialogTag = _tagCount.toString(); + _tagCount++; + } + + final dialog = Dialog(); + _dialogs[dialogTag] = dialog; + + close([res]) { + _dialogs.remove(dialogTag); + try { + dialog.complete(res); + } catch (e) { + debugPrint("Dialog complete catch error: $e"); + } + BackButtonInterceptor.removeByName(dialogTag); + } + + dialog.entry = OverlayEntry(builder: (context) { + bool innerClicked = false; + return Listener( + onPointerUp: (_) { + if (!innerClicked && clickMaskDismiss) { + close(); + } + innerClicked = false; + }, + child: Container( + color: Theme.of(context).brightness == Brightness.light + ? Colors.black12 + : Colors.black45, + child: StatefulBuilder(builder: (context, setState) { + return Listener( + onPointerUp: (_) => innerClicked = true, + child: builder(setState, close, overlayState.context), + ); + }))); + }); + overlayState.insert(dialog.entry!); + BackButtonInterceptor.add((stopDefaultButtonEvent, routeInfo) { + if (backDismiss) { + close(); + } + return true; + }, name: dialogTag); + return dialog.completer.future; + } + + String showLoading(String text, + {bool clickMaskDismiss = false, + bool showCancel = true, + VoidCallback? onCancel, + String? tag}) { + if (tag == null) { + tag = _tagCount.toString(); + _tagCount++; + } + show((setState, close, context) { + cancel() { + dismissAll(); + if (onCancel != null) { + onCancel(); + } + } + + return CustomAlertDialog( + content: Container( + constraints: const BoxConstraints(maxWidth: 240), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 30), + const Center(child: CircularProgressIndicator()), + const SizedBox(height: 20), + Center( + child: Text(translate(text), + style: const TextStyle(fontSize: 15))), + const SizedBox(height: 20), + Offstage( + offstage: !showCancel, + child: Center( + child: (isDesktop || isWebDesktop) + ? dialogButton('Cancel', onPressed: cancel) + : TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate('Cancel'), + style: const TextStyle( + color: MyTheme.accent))))) + ])), + onCancel: showCancel ? cancel : null, + ); + }, tag: tag); + return tag; + } + + void resetMobileActionsOverlay({FFI? ffi}) { + if (_mobileActionsOverlayEntry == null) return; + hideMobileActionsOverlay(); + showMobileActionsOverlay(ffi: ffi); + } + + void showMobileActionsOverlay({FFI? ffi}) { + if (_mobileActionsOverlayEntry != null) return; + final overlayState = _overlayKeyState.state; + if (overlayState == null) return; + + final overlay = makeMobileActionsOverlayEntry( + () => hideMobileActionsOverlay(), + ffi: ffi, + ); + overlayState.insert(overlay); + _mobileActionsOverlayEntry = overlay; + setMobileActionsOverlayVisible(true); + } + + void hideMobileActionsOverlay({store = true}) { + if (_mobileActionsOverlayEntry != null) { + _mobileActionsOverlayEntry!.remove(); + _mobileActionsOverlayEntry = null; + setMobileActionsOverlayVisible(false, store: store); + return; + } + } + + void toggleMobileActionsOverlay({FFI? ffi}) { + if (_mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(ffi: ffi); + } else { + hideMobileActionsOverlay(); + } + } + + bool existing(String tag) { + return _dialogs.keys.contains(tag); + } +} + +makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) { + makeMobileActions(BuildContext context, double s) { + final scale = s < 0.85 ? 0.85 : s; + final session = ffi ?? gFFI; + const double overlayW = 200; + const double overlayH = 45; + computeOverlayPosition() { + final screenW = MediaQuery.of(context).size.width; + final screenH = MediaQuery.of(context).size.height; + final left = (screenW - overlayW * scale) / 2; + final top = screenH - (overlayH + 80) * scale; + return Offset(left, top); + } + + if (draggablePositions.mobileActions.isInvalid()) { + draggablePositions.mobileActions.update(computeOverlayPosition()); + } else { + draggablePositions.mobileActions.tryAdjust(overlayW, overlayH, scale); + } + return DraggableMobileActions( + scale: scale, + position: draggablePositions.mobileActions, + width: overlayW, + height: overlayH, + onBackPressed: session.inputModel.onMobileBack, + onHomePressed: session.inputModel.onMobileHome, + onRecentPressed: session.inputModel.onMobileApps, + onHidePressed: onHide, + ); + } + + return OverlayEntry(builder: (context) { + if (isDesktop) { + final c = Provider.of(context); + return makeMobileActions(context, c.scale * 2.0); + } else { + return makeMobileActions(globalKey.currentContext!, 1.0); + } + }); +} + +void showToast(String text, + {Duration timeout = const Duration(seconds: 3), + Alignment alignment = const Alignment(0.0, 0.8)}) { + final overlayState = globalKey.currentState?.overlay; + if (overlayState == null) return; + final entry = OverlayEntry(builder: (context) { + return IgnorePointer( + child: Align( + alignment: alignment, + child: Container( + decoration: BoxDecoration( + color: MyTheme.color(context).toastBg, + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + decoration: TextDecoration.none, + fontWeight: FontWeight.w300, + fontSize: 18, + color: MyTheme.color(context).toastText), + ), + ))); + }); + overlayState.insert(entry); + Future.delayed(timeout, () { + entry.remove(); + }); +} + +// TODO +// - Remove argument "contentPadding", no need for it, all should look the same. +// - Remove "required" for argument "content". See simple confirm dialog "delete peer", only title and actions are used. No need to "content: SizedBox.shrink()". +// - Make dead code alive, transform arguments "onSubmit" and "onCancel" into correspondenting buttons "ConfirmOkButton", "CancelButton". +class CustomAlertDialog extends StatelessWidget { + const CustomAlertDialog( + {Key? key, + this.title, + this.titlePadding, + required this.content, + this.actions, + this.contentPadding, + this.contentBoxConstraints = const BoxConstraints(maxWidth: 500), + this.onSubmit, + this.onCancel}) + : super(key: key); + + final Widget? title; + final EdgeInsetsGeometry? titlePadding; + final Widget content; + final List? actions; + final double? contentPadding; + final BoxConstraints contentBoxConstraints; + final Function()? onSubmit; + final Function()? onCancel; + + @override + Widget build(BuildContext context) { + // request focus + FocusScopeNode scopeNode = FocusScopeNode(); + Future.delayed(Duration.zero, () { + if (!scopeNode.hasFocus) scopeNode.requestFocus(); + }); + bool tabTapped = false; + if (isAndroid) gFFI.invokeMethod("enable_soft_keyboard", true); + + return FocusScope( + node: scopeNode, + autofocus: true, + onKey: (node, key) { + if (key.logicalKey == LogicalKeyboardKey.escape) { + if (key is RawKeyDownEvent) { + onCancel?.call(); + } + return KeyEventResult.handled; // avoid TextField exception on escape + } else if (!tabTapped && + onSubmit != null && + (key.logicalKey == LogicalKeyboardKey.enter || + key.logicalKey == LogicalKeyboardKey.numpadEnter)) { + if (key is RawKeyDownEvent) onSubmit?.call(); + return KeyEventResult.handled; + } else if (key.logicalKey == LogicalKeyboardKey.tab) { + if (key is RawKeyDownEvent) { + scopeNode.nextFocus(); + tabTapped = true; + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: AlertDialog( + scrollable: true, + title: title, + content: ConstrainedBox( + constraints: contentBoxConstraints, + child: content, + ), + actions: actions, + titlePadding: titlePadding ?? MyTheme.dialogTitlePadding(), + contentPadding: + MyTheme.dialogContentPadding(actions: actions is List), + actionsPadding: MyTheme.dialogActionsPadding(), + buttonPadding: MyTheme.dialogButtonPadding), + ); + } +} + +Widget createDialogContent(String text) { + final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)'); + bool hasLink = linkRegExp.hasMatch(text); + + // Early return: no link, use default theme color + if (!hasLink) { + return SelectableText(text, style: const TextStyle(fontSize: 15)); + } + + final List spans = []; + int start = 0; + + linkRegExp.allMatches(text).forEach((match) { + if (match.start > start) { + spans.add(TextSpan(text: text.substring(start, match.start))); + } + spans.add(TextSpan( + text: match.group(0) ?? '', + style: const TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + String linkText = match.group(0) ?? ''; + linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), ''); + launchUrl(Uri.parse(linkText)); + }, + )); + start = match.end; + }); + + if (start < text.length) { + spans.add(TextSpan(text: text.substring(start))); + } + + return SelectableText.rich( + TextSpan( + style: const TextStyle(fontSize: 15), + children: spans, + ), + ); +} + +void msgBox(SessionID sessionId, String type, String title, String text, + String link, OverlayDialogManager dialogManager, + {bool? hasCancel, + ReconnectHandle? reconnect, + int? reconnectTimeout, + VoidCallback? onSubmit, + int? submitTimeout}) { + dialogManager.dismissAll(); + List buttons = []; + bool hasOk = false; + submit() { + dialogManager.dismissAll(); + if (onSubmit != null) { + onSubmit.call(); + } else { + // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (!type.contains("custom") && desktopType != DesktopType.portForward) { + closeConnection(); + } + } + } + + cancel() { + dialogManager.dismissAll(); + } + + jumplink() { + if (link.startsWith('http')) { + launchUrl(Uri.parse(link)); + } + } + + if (type != "connecting" && type != "success" && !type.contains("nook")) { + hasOk = true; + late final Widget btn; + if (submitTimeout != null) { + btn = _CountDownButton( + text: 'OK', + second: submitTimeout, + onPressed: submit, + submitOnTimeout: true, + ); + } else { + btn = dialogButton('OK', onPressed: submit); + } + buttons.insert(0, btn); + } + hasCancel ??= !type.contains("error") && + !type.contains("nocancel") && + type != "restarting"; + if (hasCancel) { + buttons.insert( + 0, dialogButton('Cancel', onPressed: cancel, isOutline: true)); + } + if (type.contains("hasclose")) { + buttons.insert( + 0, + dialogButton('Close', onPressed: () { + dialogManager.dismissAll(); + })); + } + if (reconnect != null && + title == "Connection Error" && + reconnectTimeout != null) { + // `enabled` is used to disable the dialog button once the button is clicked. + final enabled = true.obs; + final button = Obx(() => _CountDownButton( + text: 'Reconnect', + second: reconnectTimeout, + onPressed: enabled.isTrue + ? () { + // Disable the button + enabled.value = false; + reconnect(dialogManager, sessionId, false); + } + : null, + )); + buttons.insert(0, button); + } + if (link.isNotEmpty) { + buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink)); + } + dialogManager.show( + (setState, close, context) => CustomAlertDialog( + title: null, + content: SelectionArea(child: msgboxContent(type, title, text)), + actions: buttons, + onSubmit: hasOk ? submit : null, + onCancel: hasCancel == true ? cancel : null, + ), + tag: '$sessionId-$type-$title-$text-$link', + ); +} + +Color? _msgboxColor(String type) { + if (type == "input-password" || type == "custom-os-password") { + return Color(0xFFAD448E); + } + if (type.contains("success")) { + return Color(0xFF32bea6); + } + if (type.contains("error") || type == "re-input-password") { + return Color(0xFFE04F5F); + } + return Color(0xFF2C8CFF); +} + +Widget msgboxIcon(String type) { + IconData? iconData; + if (type.contains("error") || type == "re-input-password") { + iconData = Icons.cancel; + } + if (type.contains("success")) { + iconData = Icons.check_circle; + } + if (type == "wait-uac" || type == "wait-remote-accept-nook") { + iconData = Icons.hourglass_top; + } + if (type == 'on-uac' || type == 'on-foreground-elevated') { + iconData = Icons.admin_panel_settings; + } + if (type.contains('info')) { + iconData = Icons.info; + } + if (iconData != null) { + return Icon(iconData, size: 50, color: _msgboxColor(type)) + .marginOnly(right: 16); + } + + return Offstage(); +} + +// title should be null +Widget msgboxContent(String type, String title, String text) { + String translateText(String text) { + if (text.indexOf('Failed') == 0 && text.indexOf(': ') > 0) { + List words = text.split(': '); + for (var i = 0; i < words.length; ++i) { + words[i] = translate(words[i]); + } + text = words.join(': '); + } else { + List words = text.split(' '); + if (words.length > 1 && words[0].endsWith('_tip')) { + words[0] = translate(words[0]); + final rest = text.substring(words[0].length + 1); + text = '${words[0]} ${translate(rest)}'; + } else { + text = translate(text); + } + } + return text; + } + + return Row( + children: [ + msgboxIcon(type), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate(title), + style: TextStyle(fontSize: 21), + ).marginOnly(bottom: 10), + createDialogContent(translateText(text)), + ], + ), + ), + ], + ).marginOnly(bottom: 12); +} + +void msgBoxCommon(OverlayDialogManager dialogManager, String title, + Widget content, List buttons, + {bool hasCancel = true}) { + dialogManager.show((setState, close, context) => CustomAlertDialog( + title: Text( + translate(title), + style: TextStyle(fontSize: 21), + ), + content: content, + actions: buttons, + onCancel: hasCancel ? close : null, + )); +} + +Color str2color(String str, [alpha = 0xFF]) { + var hash = 160 << 16 + 114 << 8 + 91; + for (var i = 0; i < str.length; i += 1) { + hash = str.codeUnitAt(i) + ((hash << 5) - hash); + } + hash = hash % 16777216; + return Color((hash & 0xFF7FFF) | (alpha << 24)); +} + +Color str2color2(String str, {List existing = const []}) { + Map colorMap = { + "red": Colors.red, + "green": Colors.green, + "blue": Colors.blue, + "orange": Colors.orange, + "purple": Colors.purple, + "grey": Colors.grey, + "cyan": Colors.cyan, + "lime": Colors.lime, + "teal": Colors.teal, + "pink": Colors.pink[200]!, + "indigo": Colors.indigo, + "brown": Colors.brown, + }; + final color = colorMap[str.toLowerCase()]; + if (color != null) { + return color.withAlpha(0xFF); + } + if (str.toLowerCase() == 'yellow') { + return Colors.yellow.withAlpha(0xFF); + } + var hash = 0; + for (var i = 0; i < str.length; i++) { + hash += str.codeUnitAt(i); + } + List colorList = colorMap.values.toList(); + hash = hash % colorList.length; + var result = colorList[hash].withAlpha(0xFF); + if (existing.contains(result.value)) { + Color? notUsed = + colorList.firstWhereOrNull((e) => !existing.contains(e.value)); + if (notUsed != null) { + result = notUsed; + } + } + return result; +} + +const K = 1024; +const M = K * K; +const G = M * K; + +String readableFileSize(double size) { + if (size < K) { + return "${size.toStringAsFixed(2)} B"; + } else if (size < M) { + return "${(size / K).toStringAsFixed(2)} KB"; + } else if (size < G) { + return "${(size / M).toStringAsFixed(2)} MB"; + } else { + return "${(size / G).toStringAsFixed(2)} GB"; + } +} + +/// Flutter can't not catch PointerMoveEvent when size is 1 +/// This will happen in Android AccessibilityService Input +/// android can't init dispatching size yet ,see: https://stackoverflow.com/questions/59960451/android-accessibility-dispatchgesture-is-it-possible-to-specify-pressure-for-a +/// use this temporary solution until flutter or android fixes the bug +class AccessibilityListener extends StatelessWidget { + final Widget? child; + static final offset = 100; + + AccessibilityListener({this.child}); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: (evt) { + if (evt.size == 1) { + GestureBinding.instance.handlePointerEvent(PointerAddedEvent( + pointer: evt.pointer + offset, position: evt.position)); + GestureBinding.instance.handlePointerEvent(PointerDownEvent( + pointer: evt.pointer + offset, + size: 0.1, + position: evt.position)); + } + }, + onPointerUp: (evt) { + if (evt.size == 1) { + GestureBinding.instance.handlePointerEvent(PointerUpEvent( + pointer: evt.pointer + offset, + size: 0.1, + position: evt.position)); + GestureBinding.instance.handlePointerEvent(PointerRemovedEvent( + pointer: evt.pointer + offset, position: evt.position)); + } + }, + onPointerMove: (evt) { + if (evt.size == 1) { + GestureBinding.instance.handlePointerEvent(PointerMoveEvent( + pointer: evt.pointer + offset, + size: 0.1, + delta: evt.delta, + position: evt.position)); + } + }, + child: child); + } +} + +class AndroidPermissionManager { + static Completer? _completer; + static Timer? _timer; + static var _current = ""; + + static bool isWaitingFile() { + if (_completer != null) { + return !_completer!.isCompleted && _current == kManageExternalStorage; + } + return false; + } + + static Future check(String type) { + if (isDesktop || isWeb) { + return Future.value(true); + } + return gFFI.invokeMethod("check_permission", type); + } + + // startActivity goto Android Setting's page to request permission manually by user + static void startAction(String action) { + gFFI.invokeMethod(AndroidChannel.kStartAction, action); + } + + /// We use XXPermissions to request permissions, + /// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java + static Future request(String type) { + if (isDesktop || isWeb) { + return Future.value(true); + } + + gFFI.invokeMethod("request_permission", type); + + // clear last task + if (_completer?.isCompleted == false) { + _completer?.complete(false); + } + _timer?.cancel(); + + _current = type; + _completer = Completer(); + + _timer = Timer(Duration(seconds: 120), () { + if (_completer == null) return; + if (!_completer!.isCompleted) { + _completer!.complete(false); + } + _completer = null; + _current = ""; + }); + return _completer!.future; + } + + static complete(String type, bool res) { + if (type != _current) { + res = false; + } + _timer?.cancel(); + _completer?.complete(res); + _current = ""; + } +} + +RadioListTile getRadio( + Widget title, T toValue, T curValue, ValueChanged? onChange, + {bool? dense}) { + return RadioListTile( + visualDensity: VisualDensity.compact, + controlAffinity: ListTileControlAffinity.trailing, + title: title, + value: toValue, + groupValue: curValue, + onChanged: onChange, + dense: dense, + ); +} + +/// 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 { + debugPrint("_globalFFI init"); + _globalFFI = FFI(null); + debugPrint("_globalFFI init end"); + // after `put`, can also be globally found by Get.find(); + Get.put(_globalFFI, permanent: true); +} + +String translate(String name) { + if (name.startsWith('Failed to') && name.contains(': ')) { + return name.split(': ').map((x) => translate(x)).join(': '); + } + return platformFFI.translate(name, localeName); +} + +// This function must be kept the same as the one in rust and sciter code. +// rust: libs/hbb_common/src/config.rs -> option2bool() +// sciter: Does not have the function, but it should be kept the same. +bool option2bool(String option, String value) { + bool res; + if (option.startsWith("enable-")) { + res = value != "N"; + } else if (option.startsWith("allow-") || + option == kOptionStopService || + option == kOptionDirectServer || + option == kOptionForceAlwaysRelay) { + res = value == "Y"; + } else { + // "" is true + res = value != "N"; + } + return res; +} + +String bool2option(String option, bool b) { + String res; + if (option.startsWith('enable-') && + option != kOptionEnableUdpPunch && + option != kOptionEnableIpv6Punch) { + res = b ? defaultOptionYes : 'N'; + } else if (option.startsWith('allow-') || + option == kOptionStopService || + option == kOptionDirectServer || + option == kOptionForceAlwaysRelay) { + res = b ? 'Y' : defaultOptionNo; + } else { + res = b ? 'Y' : 'N'; + } + return res; +} + +mainSetBoolOption(String key, bool value) async { + String v = bool2option(key, value); + await bind.mainSetOption(key: key, value: v); +} + +Future mainGetBoolOption(String key) async { + return option2bool(key, await bind.mainGetOption(key: key)); +} + +bool mainGetBoolOptionSync(String key) { + return option2bool(key, bind.mainGetOptionSync(key: key)); +} + +mainSetLocalBoolOption(String key, bool value) async { + String v = bool2option(key, value); + await bind.mainSetLocalOption(key: key, value: v); +} + +bool mainGetLocalBoolOptionSync(String key) { + return option2bool(key, bind.mainGetLocalOption(key: key)); +} + +bool mainGetPeerBoolOptionSync(String id, String key) { + return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key)); +} + +// Don't use `option2bool()` and `bool2option()` to convert the session option. +// Use `sessionGetToggleOption()` and `sessionToggleOption()` instead. +// Because all session options use `Y` and `` as values. + +Future matchPeer( + String searchText, Peer peer, PeerTabIndex peerTabIndex) 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; + } + if (peer.alias.toLowerCase().contains(searchText)) { + return true; + } + if (peerTabShowNote(peerTabIndex) && + peer.note.toLowerCase().contains(searchText)) { + return true; + } + return false; +} + +/// Get the image for the current [platform]. +Widget getPlatformImage(String platform, {double size = 50}) { + if (platform.isEmpty) { + return Container(width: size, height: size); + } + if (platform == kPeerPlatformMacOS) { + platform = 'mac'; + } else if (platform != kPeerPlatformLinux && + platform != kPeerPlatformAndroid) { + platform = 'win'; + } else { + platform = platform.toLowerCase(); + } + return SvgPicture.asset('assets/$platform.svg', height: size, width: size); +} + +class LastWindowPosition { + double? width; + double? height; + double? offsetWidth; + double? offsetHeight; + bool? isMaximized; + bool? isFullscreen; + + LastWindowPosition(this.width, this.height, this.offsetWidth, + this.offsetHeight, this.isMaximized, this.isFullscreen); + + bool equals(LastWindowPosition other) { + return ((width == other.width) && + (height == other.height) && + (offsetWidth == other.offsetWidth) && + (offsetHeight == other.offsetHeight) && + (isMaximized == other.isMaximized) && + (isFullscreen == other.isFullscreen)); + } + + Map toJson() { + return { + "width": width, + "height": height, + "offsetWidth": offsetWidth, + "offsetHeight": offsetHeight, + "isMaximized": isMaximized, + "isFullscreen": isFullscreen, + }; + } + + @override + String toString() { + return jsonEncode(toJson()); + } + + static LastWindowPosition? loadFromString(String content) { + if (content.isEmpty) { + return null; + } + try { + final m = jsonDecode(content); + return LastWindowPosition(m["width"], m["height"], m["offsetWidth"], + m["offsetHeight"], m["isMaximized"], m["isFullscreen"]); + } catch (e) { + debugPrintStack( + label: + 'Failed to load LastWindowPosition "$content" ${e.toString()}'); + return null; + } + } +} + +String get windowFramePrefix => + kWindowPrefix + + (bind.isIncomingOnly() + ? "incoming_" + : (bind.isOutgoingOnly() ? "outgoing_" : "")); + +typedef WindowKey = ({WindowType type, int? windowId}); + +LastWindowPosition? _lastWindowPosition = null; +final Debouncer _saveWindowDebounce = Debouncer(delay: Duration(seconds: 1)); + +/// Save window position and size on exit +/// Note that windowId must be provided if it's subwindow +Future saveWindowPosition(WindowType type, + {int? windowId, bool? flush}) async { + if (type != WindowType.Main && windowId == null) { + debugPrint( + "Error: windowId cannot be null when saving positions for sub window"); + } + + Offset? position; + Size? sz; + late bool isMaximized; + bool isFullscreen = stateGlobal.fullscreen.isTrue; + + setPreFrame() { + final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name); + var lpos = LastWindowPosition.loadFromString(pos); + if (lpos != null) { + if (lpos.offsetWidth != null && lpos.offsetHeight != null) { + position = Offset(lpos.offsetWidth!, lpos.offsetHeight!); + } + if (lpos.width != null && lpos.height != null) { + sz = Size(lpos.width!, lpos.height!); + } + } + } + + switch (type) { + case WindowType.Main: + // Checking `bind.isIncomingOnly()` is a simple workaround for MacOS. + // `await windowManager.isMaximized()` will always return true + // if is not resizable. The reason is unknown. + // + // `setResizable(!bind.isIncomingOnly());` in main.dart + isMaximized = + bind.isIncomingOnly() ? false : await windowManager.isMaximized(); + if (isFullscreen || isMaximized) { + setPreFrame(); + } else { + position = await windowManager.getPosition( + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + sz = await windowManager.getSize( + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + break; + default: + final wc = WindowController.fromWindowId(windowId!); + isMaximized = await wc.isMaximized(); + if (isFullscreen || isMaximized) { + setPreFrame(); + } else { + final Rect frame; + try { + frame = await wc.getFrame(); + } catch (e) { + debugPrint( + "Failed to get frame of window $windowId, it may be hidden"); + return; + } + position = frame.topLeft; + sz = frame.size; + } + break; + } + if (isWindows && position != null) { + const kMinOffset = -10000; + const kMaxOffset = 10000; + if (position!.dx < kMinOffset || + position!.dy < kMinOffset || + position!.dx > kMaxOffset || + position!.dy > kMaxOffset) { + debugPrint("Invalid position: $position, ignore saving position"); + return; + } + } + + final pos = LastWindowPosition(sz?.width, sz?.height, position?.dx, + position?.dy, isMaximized, isFullscreen); + + final WindowKey key = (type: type, windowId: windowId); + + final bool haveNewWindowPosition = + (_lastWindowPosition == null) || !pos.equals(_lastWindowPosition!); + final bool isPreviousNewWindowPositionPending = _saveWindowDebounce.isRunning; + + if (haveNewWindowPosition || isPreviousNewWindowPositionPending) { + _lastWindowPosition = pos; + + if (flush ?? false) { + // If a previous update is pending, replace it. + _saveWindowDebounce.cancel(); + await _saveWindowPositionActual(key); + } else if (haveNewWindowPosition) { + _saveWindowDebounce.call(() => _saveWindowPositionActual(key)); + } + } +} + +Future _saveWindowPositionActual(WindowKey key) async { + LastWindowPosition? pos = _lastWindowPosition; + + if (pos != null) { + debugPrint( + "Saving frame: ${key.windowId}: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}"); + + await bind.setLocalFlutterOption( + k: windowFramePrefix + key.type.name, v: pos.toString()); + + if ((key.type == WindowType.RemoteDesktop || + key.type == WindowType.ViewCamera) && + key.windowId != null) { + await _saveSessionWindowPosition(key.type, key.windowId!, + pos.isMaximized ?? false, pos.isFullscreen ?? false, pos); + } + } +} + +Future _saveSessionWindowPosition(WindowType windowType, int windowId, + bool isMaximized, bool isFullscreen, LastWindowPosition pos) async { + final remoteList = await DesktopMultiWindow.invokeMethod( + windowId, kWindowEventGetRemoteList, null); + getPeerPos(String peerId) { + if (isMaximized || isFullscreen) { + final peerPos = bind.mainGetPeerFlutterOptionSync( + id: peerId, k: windowFramePrefix + windowType.name); + var lpos = LastWindowPosition.loadFromString(peerPos); + return LastWindowPosition( + lpos?.width ?? pos.offsetWidth, + lpos?.height ?? pos.offsetHeight, + lpos?.offsetWidth ?? pos.offsetWidth, + lpos?.offsetHeight ?? pos.offsetHeight, + isMaximized, + isFullscreen) + .toString(); + } else { + return pos.toString(); + } + } + + if (remoteList != null) { + for (final peerId in remoteList.split(',')) { + bind.mainSetPeerFlutterOptionSync( + id: peerId, + k: windowFramePrefix + windowType.name, + v: getPeerPos(peerId)); + } + } +} + +Future _adjustRestoreMainWindowSize(double? width, double? height) async { + const double minWidth = 1; + const double minHeight = 1; + const double maxWidth = 6480; + const double maxHeight = 6480; + + final defaultWidth = + ((isDesktop || isWebDesktop) ? 1280 : kMobileDefaultDisplayWidth) + .toDouble(); + final defaultHeight = + ((isDesktop || isWebDesktop) ? 720 : kMobileDefaultDisplayHeight) + .toDouble(); + double restoreWidth = width ?? defaultWidth; + double restoreHeight = height ?? defaultHeight; + + if (restoreWidth < minWidth) { + restoreWidth = defaultWidth; + } + if (restoreHeight < minHeight) { + restoreHeight = defaultHeight; + } + if (restoreWidth > maxWidth) { + restoreWidth = defaultWidth; + } + if (restoreHeight > maxHeight) { + restoreHeight = defaultHeight; + } + return Size(restoreWidth, restoreHeight); +} + +// Consider using Rect.contains() instead, +// though the implementation is not exactly the same. +bool isPointInRect(Offset point, Rect rect) { + return point.dx >= rect.left && + point.dx <= rect.right && + point.dy >= rect.top && + point.dy <= rect.bottom; +} + +/// return null means center +Future _adjustRestoreMainWindowOffset( + double? left, + double? top, + double? width, + double? height, +) async { + if (left == null || top == null || width == null || height == null) { + return null; + } + + if (isDesktop || isWebDesktop) { + final screens = await window_size.getScreenList(); + if (screens.isNotEmpty) { + final windowRect = Rect.fromLTWH(left, top, width, height); + bool isVisible = false; + for (final screen in screens) { + final intersection = windowRect.intersect(screen.visibleFrame); + if (intersection.width >= 10.0 && intersection.height >= 10.0) { + isVisible = true; + break; + } + } + if (!isVisible) { + return null; + } + return Offset(left, top); + } + } + + double frameLeft = 0.0; + double frameTop = 0.0; + double frameRight = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplaySize + : kMobileMaxDisplaySize) + .toDouble(); + double frameBottom = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplaySize + : kMobileMaxDisplaySize) + .toDouble(); + + final minWidth = 10.0; + if ((left + minWidth) > frameRight || + (top + minWidth) > frameBottom || + (left + width - minWidth) < frameLeft || + top < frameTop) { + return null; + } else { + return Offset(left, top); + } +} + +/// Restore window position and size on start +/// Note that windowId must be provided if it's subwindow +// +// display is used to set the offset of the window in individual display mode. +Future restoreWindowPosition(WindowType type, + {int? windowId, String? peerId, int? display}) async { + if (bind + .mainGetEnv(key: "DISABLE_RUSTDESK_RESTORE_WINDOW_POSITION") + .isNotEmpty) { + return false; + } + if (type != WindowType.Main && windowId == null) { + debugPrint( + "Error: windowId cannot be null when saving positions for sub window"); + return false; + } + + bool isRemotePeerPos = false; + String? pos; + // No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs) + // Though "open in tabs" is true and the new window restore peer position, it's ok. + if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) && + windowId != null && + peerId != null) { + final peerPos = bind.mainGetPeerFlutterOptionSync( + id: peerId, k: windowFramePrefix + type.name); + if (peerPos.isNotEmpty) { + pos = peerPos; + } + isRemotePeerPos = pos != null; + } + pos ??= bind.getLocalFlutterOption(k: windowFramePrefix + type.name); + + var lpos = LastWindowPosition.loadFromString(pos); + if (lpos == null) { + debugPrint("No window position saved, trying to center the window."); + switch (type) { + case WindowType.Main: + // Center the main window only if no position is saved (on first run). + if (isWindows || isLinux) { + await windowManager.center(); + } + // For MacOS, the window is already centered by default. + // See https://github.com/rustdesk/rustdesk/blob/9b9276e7524523d7f667fefcd0694d981443df0e/flutter/macos/Runner/Base.lproj/MainMenu.xib#L333 + // If `` in `` is not set, the window will be centered. + break; + default: + // No need to change the position of a sub window if no position is saved, + // since the default position is already centered. + // https://github.com/rustdesk/rustdesk/blob/317639169359936f7f9f85ef445ec9774218772d/flutter/lib/utils/multi_window_manager.dart#L163 + break; + } + return true; + } + if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) { + if (!isRemotePeerPos && windowId != null) { + if (lpos.offsetWidth != null) { + lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset; + } + if (lpos.offsetHeight != null) { + lpos.offsetHeight = lpos.offsetHeight! + windowId * kNewWindowOffset; + } + } + if (display != null) { + if (lpos.offsetWidth != null) { + lpos.offsetWidth = lpos.offsetWidth! + display * kNewWindowOffset; + } + if (lpos.offsetHeight != null) { + lpos.offsetHeight = lpos.offsetHeight! + display * kNewWindowOffset; + } + } + } + + final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height); + final offsetLeftTop = await _adjustRestoreMainWindowOffset( + lpos.offsetWidth, + lpos.offsetHeight, + size.width, + size.height, + ); + debugPrint( + "restore lpos: ${size.width}/${size.height}, offset:${offsetLeftTop?.dx}/${offsetLeftTop?.dy}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}"); + + switch (type) { + case WindowType.Main: + restorePos() async { + if (offsetLeftTop == null) { + await windowManager.center(); + } else { + await windowManager.setPosition(offsetLeftTop, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + } + if (lpos.isMaximized == true) { + await restorePos(); + if (!(bind.isIncomingOnly() || bind.isOutgoingOnly())) { + await windowManager.maximize(); + } + } else { + final storeSize = !bind.isIncomingOnly() || bind.isOutgoingOnly(); + if (isWindows) { + if (storeSize) { + // We need to set the window size first to avoid the incorrect size in some special cases. + // E.g. There are two monitors, the left one is 100% DPI and the right one is 175% DPI. + // The window belongs to the left monitor, but if it is moved a little to the right, it will belong to the right monitor. + // After restoring, the size will be incorrect. + // See known issue in https://github.com/rustdesk/rustdesk/pull/9840 + await windowManager.setSize(size, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + await restorePos(); + if (storeSize) { + await windowManager.setSize(size, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + } else { + if (storeSize) { + await windowManager.setSize(size, + ignoreDevicePixelRatio: _ignoreDevicePixelRatio); + } + await restorePos(); + } + } + return true; + default: + final wc = WindowController.fromWindowId(windowId!); + restoreFrame() async { + if (offsetLeftTop == null) { + await wc.center(); + } else { + final frame = Rect.fromLTWH( + offsetLeftTop.dx, offsetLeftTop.dy, size.width, size.height); + await wc.setFrame(frame); + } + } + if (lpos.isFullscreen == true) { + if (!isMacOS) { + await restoreFrame(); + } + // An duration is needed to avoid the window being restored after fullscreen. + Future.delayed(Duration(milliseconds: 300), () async { + if (kWindowId == windowId) { + stateGlobal.setFullscreen(true); + } else { + // If is not current window, we need to send a fullscreen message to `windowId` + DesktopMultiWindow.invokeMethod( + windowId, kWindowEventSetFullscreen, 'true'); + } + }); + } else if (lpos.isMaximized == true) { + await restoreFrame(); + // An duration is needed to avoid the window being restored after maximized. + Future.delayed(Duration(milliseconds: 300), () async { + await wc.maximize(); + }); + } else { + await restoreFrame(); + } + break; + } + return false; +} + +var webInitialLink = ""; + +/// Initialize uni links for macos/windows +/// +/// [Availability] +/// initUniLinks should only be used on macos/windows. +/// we use dbus for linux currently. +Future initUniLinks() async { + if (isLinux) { + return false; + } + // check cold boot + try { + final initialLink = await getInitialLink(); + print("initialLink: $initialLink"); + if (initialLink == null || initialLink.isEmpty) { + return false; + } + if (isWeb) { + webInitialLink = initialLink; + return false; + } else { + return handleUriLink(uriString: initialLink); + } + } catch (err) { + debugPrintStack(label: "$err"); + return false; + } +} + +/// Listen for uni links. +/// +/// * handleByFlutter: Should uni links be handled by Flutter. +/// +/// Returns a [StreamSubscription] which can listen the uni links. +StreamSubscription? listenUniLinks({handleByFlutter = true}) { + if (isLinux || isWeb) { + return null; + } + + final sub = uriLinkStream.listen((Uri? uri) { + debugPrint("A uri was received: $uri. handleByFlutter $handleByFlutter"); + if (uri != null) { + if (handleByFlutter) { + handleUriLink(uri: uri); + } else { + bind.sendUrlScheme(url: uri.toString()); + } + } else { + print("uni listen error: uri is empty."); + } + }, onError: (err) { + print("uni links error: $err"); + }); + return sub; +} + +enum UriLinkType { + remoteDesktop, + fileTransfer, + viewCamera, + portForward, + rdp, + terminal, +} + +setEnvTerminalAdmin() { + bind.mainSetEnv(key: 'IS_TERMINAL_ADMIN', value: 'Y'); +} + +// uri link handler +bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { + List? args; + if (cmdArgs != null && cmdArgs.isNotEmpty) { + args = cmdArgs; + // rustdesk + if (args[0].startsWith(bind.mainUriPrefixSync())) { + final uri = Uri.tryParse(args[0]); + if (uri != null) { + args = urlLinkToCmdArgs(uri); + } + } + } else if (uri != null) { + args = urlLinkToCmdArgs(uri); + } else if (uriString != null) { + final uri = Uri.tryParse(uriString); + if (uri != null) { + args = urlLinkToCmdArgs(uri); + } + } + if (args == null) { + return false; + } + + if (args.isEmpty) { + windowOnTop(null); + return true; + } + + UriLinkType? type; + String? id; + String? password; + String? switchUuid; + bool? forceRelay; + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case '--connect': + case '--play': + type = UriLinkType.remoteDesktop; + id = args[i + 1]; + i++; + break; + case '--file-transfer': + type = UriLinkType.fileTransfer; + id = args[i + 1]; + i++; + break; + case '--view-camera': + type = UriLinkType.viewCamera; + id = args[i + 1]; + i++; + break; + case '--port-forward': + type = UriLinkType.portForward; + id = args[i + 1]; + i++; + break; + case '--rdp': + type = UriLinkType.rdp; + id = args[i + 1]; + i++; + break; + case '--terminal': + type = UriLinkType.terminal; + id = args[i + 1]; + i++; + break; + case '--terminal-admin': + setEnvTerminalAdmin(); + type = UriLinkType.terminal; + id = args[i + 1]; + i++; + break; + case '--password': + password = args[i + 1]; + i++; + break; + case '--switch_uuid': + switchUuid = args[i + 1]; + i++; + break; + case '--relay': + forceRelay = true; + break; + default: + break; + } + } + if (type != null && id != null) { + switch (type) { + case UriLinkType.remoteDesktop: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newRemoteDesktop(id!, + password: password, + switchUuid: switchUuid, + forceRelay: forceRelay); + }); + break; + case UriLinkType.fileTransfer: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newFileTransfer(id!, + password: password, forceRelay: forceRelay); + }); + break; + case UriLinkType.viewCamera: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newViewCamera(id!, + password: password, forceRelay: forceRelay); + }); + break; + case UriLinkType.portForward: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newPortForward(id!, false, + password: password, forceRelay: forceRelay); + }); + break; + case UriLinkType.rdp: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newPortForward(id!, true, + password: password, forceRelay: forceRelay); + }); + break; + case UriLinkType.terminal: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newTerminal(id!, + password: password, forceRelay: forceRelay); + }); + break; + } + + return true; + } + + return false; +} + +List? urlLinkToCmdArgs(Uri uri) { + String? command; + String? id; + final options = [ + "connect", + "play", + "file-transfer", + "view-camera", + "port-forward", + "rdp", + "terminal", + "terminal-admin", + ]; + if (uri.authority.isEmpty && + uri.path.split('').every((char) => char == '/')) { + return []; + } else if (uri.authority == "connection" && uri.path.startsWith("/new/")) { + // For compatibility + command = '--connect'; + id = uri.path.substring("/new/".length); + } else if (uri.authority == "config") { + if (isAndroid || isIOS) { + final config = uri.path.substring("/".length); + // add a timer to make showToast work + Timer(Duration(seconds: 1), () { + importConfig(null, null, config); + }); + } + return null; + } else if (uri.authority == "password") { + if (isAndroid || isIOS) { + final password = uri.path.substring("/".length); + if (password.isNotEmpty) { + Timer(Duration(seconds: 1), () async { + final ok = + await bind.mainSetPermanentPasswordWithResult(password: password); + showToast(translate(ok ? 'Successful' : 'Failed')); + }); + } + } + } else if (options.contains(uri.authority)) { + command = '--${uri.authority}'; + if (uri.path.length > 1) { + id = uri.path.substring(1); + } + } else if (uri.authority.length > 2 && + (uri.path.length <= 1 || + (uri.path == '/r' || uri.path.startsWith('/r@')))) { + // rustdesk:// + // rustdesk:///r + // rustdesk:///r@ + command = '--connect'; + id = uri.authority; + if (uri.path.length > 1) { + id = id + uri.path; + } + } + + var queryParameters = + uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v)); + + var key = queryParameters["key"]; + if (id != null) { + if (key != null) { + id = "$id?key=$key"; + } + } + + if (isMobile && id != null) { + final forceRelay = queryParameters["relay"] != null; + final password = queryParameters["password"]; + + // Determine connection type based on command + if (command == '--file-transfer') { + connect(Get.context!, id, + isFileTransfer: true, forceRelay: forceRelay, password: password); + } else if (command == '--view-camera') { + connect(Get.context!, id, + isViewCamera: true, forceRelay: forceRelay, password: password); + } else if (command == '--terminal') { + connect(Get.context!, id, + isTerminal: true, forceRelay: forceRelay, password: password); + } else if (command == 'terminal-admin') { + setEnvTerminalAdmin(); + connect(Get.context!, id, + isTerminal: true, forceRelay: forceRelay, password: password); + } else { + // Default to remote desktop for '--connect', '--play', or direct connection + connect(Get.context!, id, forceRelay: forceRelay, password: password); + } + return null; + } + + List args = List.empty(growable: true); + if (command != null && id != null) { + args.add(command); + args.add(id); + var param = queryParameters; + String? password = param["password"]; + if (password != null) args.addAll(['--password', password]); + String? switch_uuid = param["switch_uuid"]; + if (switch_uuid != null) args.addAll(['--switch_uuid', switch_uuid]); + if (param["relay"] != null) args.add("--relay"); + return args; + } + + return null; +} + +connectMainDesktop(String id, + {required bool isFileTransfer, + required bool isViewCamera, + required bool isTerminal, + required bool isTcpTunneling, + required bool isRDP, + bool? forceRelay, + String? password, + String? connToken, + bool? isSharedPassword}) async { + if (isFileTransfer) { + await rustDeskWinManager.newFileTransfer(id, + password: password, + isSharedPassword: isSharedPassword, + connToken: connToken, + forceRelay: forceRelay); + } else if (isViewCamera) { + await rustDeskWinManager.newViewCamera(id, + password: password, + isSharedPassword: isSharedPassword, + connToken: connToken, + forceRelay: forceRelay); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.newPortForward(id, isRDP, + password: password, + isSharedPassword: isSharedPassword, + connToken: connToken, + forceRelay: forceRelay); + } else if (isTerminal) { + await rustDeskWinManager.newTerminal(id, + password: password, + isSharedPassword: isSharedPassword, + connToken: connToken, + forceRelay: forceRelay); + } else { + await rustDeskWinManager.newRemoteDesktop(id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay); + } +} + +/// Connect to a peer with [id]. +/// If [isFileTransfer], starts a session only for file transfer. +/// If [isViewCamera], starts a session only for view camera. +/// If [isTcpTunneling], starts a session only for tcp tunneling. +/// If [isRDP], starts a session only for rdp. +connect(BuildContext context, String id, + {bool isFileTransfer = false, + bool isViewCamera = false, + bool isTerminal = false, + bool isTcpTunneling = false, + bool isRDP = false, + bool forceRelay = false, + String? password, + String? connToken, + bool? isSharedPassword}) async { + if (id == '') return; + if (!isDesktop || desktopType == DesktopType.main) { + try { + if (Get.isRegistered()) { + final idController = Get.find(); + idController.text = formatID(id); + } + if (Get.isRegistered()) { + final fieldTextEditingController = Get.find(); + fieldTextEditingController.text = formatID(id); + } + } catch (_) {} + } + id = id.replaceAll(' ', ''); + final oldId = id; + id = await bind.mainHandleRelayId(id: id); + forceRelay = id != oldId || forceRelay; + assert(!(isFileTransfer && isTcpTunneling && isRDP), + "more than one connect type"); + + if (isDesktop) { + if (desktopType == DesktopType.main) { + await connectMainDesktop( + id, + isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, + isTerminal: isTerminal, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay, + ); + } else { + await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { + 'id': id, + 'isFileTransfer': isFileTransfer, + 'isViewCamera': isViewCamera, + 'isTerminal': isTerminal, + 'isTcpTunneling': isTcpTunneling, + 'isRDP': isRDP, + 'password': password, + 'isSharedPassword': isSharedPassword, + 'forceRelay': forceRelay, + 'connToken': connToken, + }); + } + } else { + if (isFileTransfer) { + if (isAndroid) { + if (!await AndroidPermissionManager.check(kManageExternalStorage)) { + if (!await AndroidPermissionManager.request(kManageExternalStorage)) { + return; + } + } + } + if (isWeb) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + desktop_file_manager.FileManagerPage( + id: id, + password: password, + isSharedPassword: isSharedPassword), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => FileManagerPage( + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay), + ), + ); + } + } else if (isViewCamera) { + if (isWeb) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + desktop_view_camera.ViewCameraPage( + key: ValueKey(id), + id: id, + toolbarState: ToolbarState(), + password: password, + isSharedPassword: isSharedPassword, + ), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ViewCameraPage( + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay), + ), + ); + } + } else if (isTerminal) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => TerminalPage( + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay, + ), + ), + ); + } else { + if (isWeb) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => desktop_remote.RemotePage( + key: ValueKey(id), + id: id, + toolbarState: ToolbarState(), + password: password, + isSharedPassword: isSharedPassword, + ), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => RemotePage( + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay), + ), + ); + } + } + stateGlobal.isInMainPage = false; + } + + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } +} + +Map getHttpHeaders() { + return { + 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}' + }; +} + +// Simple wrapper of built-in types for reference use. +class SimpleWrapper { + T value; + SimpleWrapper(this.value); +} + +/// Wakelock manager with reference counting for desktop. +/// Ensures wakelock is only disabled when all sessions are closed/minimized. +/// +/// Note: Each isolate has its own WakelockPlus instance with independent assertion. +/// As long as one isolate has wakelock enabled, the screen stays awake. +/// This manager handles multiple tabs within the same isolate. +class WakelockManager { + static final Set _enabledKeys = {}; + // Don't use WakelockPlus.enabled, it causes error on Android: + // Unhandled Exception: FormatException: Message corrupted + // + // On Linux, multiple enable() calls create only one inhibit, but each disable() + // only releases if _cookie != null. So we need our own _enabled state to avoid + // calling disable() when not enabled. + // See: https://github.com/fluttercommunity/wakelock_plus/blob/0c74e5bbc6aefac57b6c96bb7ef987705ed559ec/wakelock_plus/lib/src/wakelock_plus_linux_plugin.dart#L48 + static bool _enabled = false; + + static void enable(UniqueKey key, {bool isServer = false}) { + // Check if we should keep awake during outgoing sessions + if (!isServer) { + final keepAwake = + mainGetLocalBoolOptionSync(kOptionKeepAwakeDuringOutgoingSessions); + if (!keepAwake) { + return; // Don't enable wakelock if user disabled keep awake + } + } + if (isDesktop) { + _enabledKeys.add(key); + } + if (!_enabled) { + _enabled = true; + WakelockPlus.enable(); + } + } + + static void disable(UniqueKey key) { + if (isDesktop) { + _enabledKeys.remove(key); + if (_enabledKeys.isNotEmpty) { + return; + } + } + if (_enabled) { + WakelockPlus.disable(); + _enabled = false; + } + } +} + +/// call this to reload current window. +/// +/// [Note] +/// Must have [RefreshWrapper] on the top of widget tree. +void reloadCurrentWindow() { + if (Get.context != null) { + // reload self window + RefreshWrapper.of(Get.context!)?.rebuild(); + } else { + debugPrint( + "reload current window failed, global BuildContext does not exist"); + } +} + +/// call this to reload all windows, including main + all sub windows. +Future reloadAllWindows() async { + reloadCurrentWindow(); + try { + final ids = await DesktopMultiWindow.getAllSubWindowIds(); + for (final id in ids) { + DesktopMultiWindow.invokeMethod(id, kWindowActionRebuild); + } + } on AssertionError { + // ignore + } +} + +/// Indicate the flutter app is running in portable mode. +/// +/// [Note] +/// Portable build is only available on Windows. +bool isRunningInPortableMode() { + if (!isWindows) { + return false; + } + return bool.hasEnvironment(kEnvPortableExecutable); +} + +/// Window status callback +Future onActiveWindowChanged() async { + print( + "[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}"); + if (rustDeskWinManager.getActiveWindows().isEmpty) { + // close all sub windows + try { + if (isLinux) { + await Future.wait([ + saveWindowPosition(WindowType.Main), + rustDeskWinManager.closeAllSubWindows() + ]); + } else { + await rustDeskWinManager.closeAllSubWindows(); + } + } catch (err) { + debugPrintStack(label: "$err"); + } finally { + debugPrint("Start closing RustDesk..."); + await windowManager.setPreventClose(false); + await windowManager.close(); + if (isMacOS) { + // If we call without delay, `flutter/macos/Runner/MainFlutterWindow.swift` can handle the "terminate" event. + // But the app will not close. + // + // No idea why we need to delay here, `terminate()` itself is also an async function. + // + // A quick workaround, use `Timer.periodic` to avoid the app not closing. + // Because `await windowManager.close()` and `RdPlatformChannel.instance.terminate()` + // may not work since `Flutter 3.24.4`, see the following logs. + // A delay will allow the app to close. + // + //``` + // embedder.cc (2725): 'FlutterPlatformMessageCreateResponseHandle' returned 'kInvalidArguments'. Engine handle was invalid. + // 2024-11-11 11:41:11.546 RustDesk[90272:2567686] Failed to create a FlutterPlatformMessageResponseHandle (2) + // embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle. + // 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2). + // ``` + periodic_immediate( + Duration(milliseconds: 30), RdPlatformChannel.instance.terminate); + } + } + } +} + +Timer periodic_immediate(Duration duration, Future Function() callback) { + Future.delayed(Duration.zero, callback); + return Timer.periodic(duration, (timer) async { + await callback(); + }); +} + +/// return a human readable windows version +WindowsTarget getWindowsTarget(int buildNumber) { + if (!isWindows) { + return WindowsTarget.naw; + } + if (buildNumber >= 22000) { + return WindowsTarget.w11; + } else if (buildNumber >= 10240) { + return WindowsTarget.w10; + } else if (buildNumber >= 9600) { + return WindowsTarget.w8_1; + } else if (buildNumber >= 9200) { + return WindowsTarget.w8; + } else if (buildNumber >= 7601) { + return WindowsTarget.w7; + } else if (buildNumber >= 6002) { + return WindowsTarget.vista; + } else { + // minimum support + return WindowsTarget.xp; + } +} + +/// Get windows target build number. +/// +/// [Note] +/// Please use this function wrapped with `Platform.isWindows`. +int getWindowsTargetBuildNumber() { + return getWindowsTargetBuildNumber_(); +} + +/// Indicating we need to use compatible ui mode. +/// +/// [Conditions] +/// - Windows 7, window will overflow when we use frameless ui. +bool get kUseCompatibleUiMode => + isWindows && + const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion); + +bool get isWin10 => windowsBuildNumber.windowsVersion == WindowsTarget.w10; + +class ServerConfig { + late String idServer; + late String relayServer; + late String apiServer; + late String key; + + ServerConfig( + {String? idServer, String? relayServer, String? apiServer, String? key}) { + this.idServer = idServer?.trim() ?? ''; + this.relayServer = relayServer?.trim() ?? ''; + this.apiServer = apiServer?.trim() ?? ''; + this.key = key?.trim() ?? ''; + } + + /// decode from shared string (from user shared or rustdesk-server generated) + /// also see [encode] + /// throw when decoding failure + ServerConfig.decode(String msg) { + var json = {}; + try { + // back compatible + json = jsonDecode(msg); + } catch (err) { + final input = msg.split('').reversed.join(''); + final bytes = base64Decode(base64.normalize(input)); + json = jsonDecode(utf8.decode(bytes, allowMalformed: true)); + } + idServer = json['host'] ?? ''; + relayServer = json['relay'] ?? ''; + apiServer = json['api'] ?? ''; + key = json['key'] ?? ''; + } + + /// encode to shared string + /// also see [ServerConfig.decode] + String encode() { + Map config = {}; + config['host'] = idServer.trim(); + config['relay'] = relayServer.trim(); + config['api'] = apiServer.trim(); + config['key'] = key.trim(); + return base64UrlEncode(Uint8List.fromList(jsonEncode(config).codeUnits)) + .split('') + .reversed + .join(); + } + + /// from local options + ServerConfig.fromOptions(Map options) + : idServer = options['custom-rendezvous-server'] ?? "", + relayServer = options['relay-server'] ?? "", + apiServer = options['api-server'] ?? "", + key = options['key'] ?? ""; +} + +Widget dialogButton(String text, + {required VoidCallback? onPressed, + bool isOutline = false, + Widget? icon, + TextStyle? style, + ButtonStyle? buttonStyle}) { + if (isDesktop || isWebDesktop) { + if (isOutline) { + return icon == null + ? OutlinedButton( + onPressed: onPressed, + child: Text(translate(text), style: style), + ) + : OutlinedButton.icon( + icon: icon, + onPressed: onPressed, + label: Text(translate(text), style: style), + ); + } else { + return icon == null + ? ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle), + onPressed: onPressed, + child: Text(translate(text), style: style), + ) + : ElevatedButton.icon( + icon: icon, + style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle), + onPressed: onPressed, + label: Text(translate(text), style: style), + ); + } + } else { + return TextButton( + onPressed: onPressed, + child: Text( + translate(text), + style: style, + ), + ); + } +} + +int versionCmp(String v1, String v2) { + return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2); +} + +String getWindowName({WindowType? overrideType}) { + final name = bind.mainGetAppNameSync(); + switch (overrideType ?? kWindowType) { + case WindowType.Main: + return name; + case WindowType.FileTransfer: + return "File Transfer - $name"; + case WindowType.ViewCamera: + return "View Camera - $name"; + case WindowType.PortForward: + return "Port Forward - $name"; + case WindowType.RemoteDesktop: + return "Remote Desktop - $name"; + default: + break; + } + return name; +} + +String getWindowNameWithId(String id, {WindowType? overrideType}) { + return "${DesktopTab.tablabelGetter(id).value} - ${getWindowName(overrideType: overrideType)}"; +} + +Future updateSystemWindowTheme() async { + // Set system window theme for macOS. + final userPreference = MyTheme.getThemeModePreference(); + if (userPreference != ThemeMode.system) { + if (isMacOS) { + await RdPlatformChannel.instance.changeSystemWindowTheme( + userPreference == ThemeMode.light + ? SystemWindowTheme.light + : SystemWindowTheme.dark); + } + } +} + +/// macOS only +/// +/// Note: not found a general solution for rust based AVFoundation bingding. +/// [AVFoundation] crate has compile error. +const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/host"); + +enum PermissionAuthorizeType { + undetermined, + authorized, + denied, // and restricted +} + +Future osxCanRecordAudio() async { + int res = await kMacOSPermChannel.invokeMethod("canRecordAudio"); + print(res); + if (res > 0) { + return PermissionAuthorizeType.authorized; + } else if (res == 0) { + return PermissionAuthorizeType.undetermined; + } else { + return PermissionAuthorizeType.denied; + } +} + +Future osxRequestAudio() async { + return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); +} + +Widget futureBuilder( + {required Future? future, required Widget Function(dynamic data) hasData}) { + return FutureBuilder( + future: future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return hasData(snapshot.data!); + } else { + if (snapshot.hasError) { + debugPrint(snapshot.error.toString()); + } + return Container(); + } + }); +} + +void onCopyFingerprint(String value) { + if (value.isNotEmpty) { + Clipboard.setData(ClipboardData(text: value)); + showToast('$value\n${translate("Copied")}'); + } else { + showToast(translate("no fingerprints")); + } +} + +Future callMainCheckSuperUserPermission() async { + bool checked = await bind.mainCheckSuperUserPermission(); + if (isMacOS) { + await windowManager.show(); + } + return checked; +} + +Future start_service(bool is_start) async { + bool checked = !bind.mainIsInstalled() || + !isMacOS || + await callMainCheckSuperUserPermission(); + if (checked) { + mainSetBoolOption(kOptionStopService, !is_start); + } +} + +Future canBeBlocked() async { + if (isWeb) { + // Web can only act as a controller, never as a controlled side, + // so it should never be blocked by a remote session. + return false; + } + // First check control permission + final controlPermission = await bind.mainGetCommon( + key: "is-remote-modify-enabled-by-control-permissions"); + if (controlPermission == "true") { + return false; + } else if (controlPermission == "false") { + return true; + } + + // Check local settings + var accessMode = await bind.mainGetOption(key: kOptionAccessMode); + var isCustomAccessMode = accessMode != 'full' && accessMode != 'view'; + var option = option2bool(kOptionAllowRemoteConfigModification, + await bind.mainGetOption(key: kOptionAllowRemoteConfigModification)); + return accessMode == 'view' || (isCustomAccessMode && !option); +} + +// to-do: web not implemented +Future shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async { + if (use != null && !await use()) { + block.value = false; + return; + } + var time0 = DateTime.now().millisecondsSinceEpoch; + await bind.mainCheckMouseTime(); + Timer(const Duration(milliseconds: 120), () async { + var d = time0 - await bind.mainGetMouseTime(); + if (d < 120) { + block.value = true; + } else { + block.value = false; + } + }); +} + +typedef WhetherUseRemoteBlock = Future Function(); +Widget buildRemoteBlock( + {required Widget child, + required RxBool block, + required bool mask, + WhetherUseRemoteBlock? use}) { + return Obx(() => MouseRegion( + onEnter: (_) async { + await shouldBeBlocked(block, use); + }, + onExit: (event) => block.value = false, + child: Stack(children: [ + // scope block tab + preventMouseKeyBuilder(child: child, block: block.value), + // mask block click, cm not block click and still use check_click_time to avoid block local click + if (mask) + Offstage( + offstage: !block.value, + child: Container( + color: Colors.black.withOpacity(0.5), + )), + ]), + )); +} + +Widget preventMouseKeyBuilder({required Widget child, required bool block}) { + return ExcludeFocus( + excluding: block, child: AbsorbPointer(child: child, absorbing: block)); +} + +Widget unreadMessageCountBuilder(RxInt? count, + {double? size, double? fontSize}) { + return Obx(() => Offstage( + offstage: !((count?.value ?? 0) > 0), + child: Container( + width: size ?? 16, + height: size ?? 16, + decoration: BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: Center( + child: Text("${count?.value ?? 0}", + maxLines: 1, + style: TextStyle(color: Colors.white, fontSize: fontSize ?? 10)), + ), + ))); +} + +Widget unreadTopRightBuilder(RxInt? count, {Widget? icon}) { + return Stack( + children: [ + icon ?? Icon(Icons.chat), + Positioned( + top: 0, + right: 0, + child: unreadMessageCountBuilder(count, size: 12, fontSize: 8)) + ], + ); +} + +String toCapitalized(String s) { + if (s.isEmpty) { + return s; + } + return s.substring(0, 1).toUpperCase() + s.substring(1); +} + +Widget buildErrorBanner(BuildContext context, + {required RxBool loading, + required RxString err, + required Function? retry, + required Function close}) { + return Obx(() => Offstage( + offstage: !(!loading.value && err.value.isNotEmpty), + child: Center( + child: Container( + color: MyTheme.color(context).errorBannerBg, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FittedBox( + child: Icon( + Icons.info, + color: Color.fromARGB(255, 249, 81, 81), + ), + ).marginAll(4), + Flexible( + child: Align( + alignment: Alignment.centerLeft, + child: Tooltip( + message: translate(err.value), + child: SelectableText( + translate(err.value), + ), + )).marginSymmetric(vertical: 2), + ), + if (retry != null) + InkWell( + onTap: () { + retry.call(); + }, + child: Text( + translate("Retry"), + style: TextStyle(color: MyTheme.accent), + )).marginSymmetric(horizontal: 5), + FittedBox( + child: InkWell( + onTap: () { + close.call(); + }, + child: Icon(Icons.close).marginSymmetric(horizontal: 5), + ), + ).marginAll(4) + ], + ), + )).marginOnly(bottom: 14), + )); +} + +String getDesktopTabLabel(String peerId, String alias) { + String label = alias.isEmpty ? peerId : alias; + try { + String peer = bind.mainGetPeerSync(id: peerId); + Map config = jsonDecode(peer); + if (config['info']['hostname'] is String) { + String hostname = config['info']['hostname']; + if (hostname.isNotEmpty && + !label.toLowerCase().contains(hostname.toLowerCase())) { + label += "@$hostname"; + } + } + } catch (e) { + debugPrint("Failed to get hostname:$e"); + } + return label; +} + +sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async { + if (pi.currentDisplay == kAllDisplayValue) { + for (int i = 0; i < pi.displays.length; i++) { + await bind.sessionRefresh(sessionId: sessionId, display: i); + } + } else { + await bind.sessionRefresh(sessionId: sessionId, display: pi.currentDisplay); + } +} + +Future> getScreenListWayland() async { + final screenRectList = []; + if (isMainDesktopWindow) { + for (var screen in await window_size.getScreenList()) { + final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor; + double l = screen.frame.left; + double t = screen.frame.top; + double r = screen.frame.right; + double b = screen.frame.bottom; + final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale); + screenRectList.add(rect); + } + } else { + final screenList = await rustDeskWinManager.call( + WindowType.Main, kWindowGetScreenList, ''); + try { + for (var screen in jsonDecode(screenList.result) as List) { + final scale = kIgnoreDpi ? 1.0 : screen['scaleFactor']; + double l = screen['frame']['l']; + double t = screen['frame']['t']; + double r = screen['frame']['r']; + double b = screen['frame']['b']; + final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale); + screenRectList.add(rect); + } + } catch (e) { + debugPrint('Failed to parse screenList: $e'); + } + } + return screenRectList; +} + +Future> getScreenListNotWayland() async { + final screenRectList = []; + final displays = bind.mainGetDisplays(); + if (displays.isEmpty) { + return screenRectList; + } + try { + for (var display in jsonDecode(displays) as List) { + // to-do: scale factor ? + // final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor; + double l = display['x'].toDouble(); + double t = display['y'].toDouble(); + double r = (display['x'] + display['w']).toDouble(); + double b = (display['y'] + display['h']).toDouble(); + screenRectList.add(Rect.fromLTRB(l, t, r, b)); + } + } catch (e) { + debugPrint('Failed to parse displays: $e'); + } + return screenRectList; +} + +Future> getScreenRectList() async { + return bind.mainCurrentIsWayland() + ? await getScreenListWayland() + : await getScreenListNotWayland(); +} + +openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi, + {bool updateCursorPos = true}) { + final displays = i == kAllDisplayValue + ? List.generate(pi.displays.length, (index) => index) + : [i]; + // Try clear image model before switching from all displays + // 1. The remote side has multiple displays. + // 2. Do not use texture render. + // 3. Connect to Display 1. + // 4. Switch to multi-displays `kAllDisplayValue` + // 5. Switch to Display 2. + // Then the remote page will display last picture of Display 1 at the beginning. + if (pi.forceTextureRender && i != kAllDisplayValue) { + ffi.imageModel.clearImage(); + } + bind.sessionSwitchDisplay( + isDesktop: isDesktop, + sessionId: ffi.sessionId, + value: Int32List.fromList(displays), + ); + ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, ffi.id, + updateCursorPos: updateCursorPos); +} + +// Open new tab or window to show this monitor. +// For now just open new window. +// +// screenRect is used to move the new window to the specified screen and set fullscreen. +openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi, + {Rect? screenRect}) { + final args = { + 'window_id': stateGlobal.windowId, + 'peer_id': peerId, + 'display': i, + 'display_count': pi.displays.length, + 'window_type': (kWindowType ?? WindowType.RemoteDesktop).index, + }; + if (screenRect != null) { + args['screen_rect'] = { + 'l': screenRect.left, + 't': screenRect.top, + 'r': screenRect.right, + 'b': screenRect.bottom, + }; + } + DesktopMultiWindow.invokeMethod( + kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args)); +} + +setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount, + WindowType windowType, int? display, Rect? screenRect) async { + if (screenRect == null) { + // Do not restore window position to new connection if there's a pre-session. + // https://github.com/rustdesk/rustdesk/discussions/8825 + if (preSessionCount == 0) { + await restoreWindowPosition(windowType, + windowId: windowId, display: display, peerId: peerId); + } + } else { + await tryMoveToScreenAndSetFullscreen(screenRect); + } +} + +tryMoveToScreenAndSetFullscreen(Rect? screenRect) async { + if (screenRect == null) { + return; + } + final wc = WindowController.fromWindowId(stateGlobal.windowId); + final curFrame = await wc.getFrame(); + final frame = + Rect.fromLTWH(screenRect.left + 30, screenRect.top + 30, 600, 400); + if (stateGlobal.fullscreen.isTrue && + curFrame.left <= frame.left && + curFrame.top <= frame.top && + curFrame.width >= frame.width && + curFrame.height >= frame.height) { + return; + } + await wc.setFrame(frame); + // An duration is needed to avoid the window being restored after fullscreen. + Future.delayed(Duration(milliseconds: 300), () async { + stateGlobal.setFullscreen(true); + }); +} + +parseParamScreenRect(Map params) { + Rect? screenRect; + if (params['screen_rect'] != null) { + double l = params['screen_rect']['l']; + double t = params['screen_rect']['t']; + double r = params['screen_rect']['r']; + double b = params['screen_rect']['b']; + screenRect = Rect.fromLTRB(l, t, r, b); + } + return screenRect; +} + +get isInputSourceFlutter => stateGlobal.getInputSource() == "Input source 2"; + +class _CountDownButton extends StatefulWidget { + _CountDownButton({ + Key? key, + required this.text, + required this.second, + required this.onPressed, + this.submitOnTimeout = false, + }) : super(key: key); + final String text; + final VoidCallback? onPressed; + final int second; + final bool submitOnTimeout; + + @override + State<_CountDownButton> createState() => _CountDownButtonState(); +} + +class _CountDownButtonState extends State<_CountDownButton> { + late int _countdownSeconds = widget.second; + + Timer? _timer; + + @override + void initState() { + super.initState(); + _startCountdownTimer(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + void _startCountdownTimer() { + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + if (_countdownSeconds <= 0) { + timer.cancel(); + if (widget.submitOnTimeout) { + widget.onPressed?.call(); + } + } else { + setState(() { + _countdownSeconds--; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return dialogButton( + '${translate(widget.text)} (${_countdownSeconds}s)', + onPressed: widget.onPressed, + isOutline: true, + ); + } +} + +importConfig(List? controllers, List? errMsgs, + String? text) { + text = text?.trim(); + if (text != null && text.isNotEmpty) { + try { + final sc = ServerConfig.decode(text); + if (isWeb || isIOS) { + sc.relayServer = ''; + } + if (sc.idServer.isNotEmpty) { + Future success = setServerConfig(controllers, errMsgs, sc); + success.then((value) { + if (value) { + showToast(translate('Import server configuration successfully')); + } else { + showToast(translate('Invalid server configuration')); + } + }); + } else { + showToast(translate('Invalid server configuration')); + } + return sc; + } catch (e) { + showToast(translate('Invalid server configuration')); + } + } else { + showToast(translate('Clipboard is empty')); + } +} + +Future setServerConfig( + List? controllers, + List? errMsgs, + ServerConfig config, +) async { + String removeEndSlash(String input) { + if (input.endsWith('/')) { + return input.substring(0, input.length - 1); + } + return input; + } + + config.idServer = removeEndSlash(config.idServer.trim()); + config.relayServer = removeEndSlash(config.relayServer.trim()); + config.apiServer = removeEndSlash(config.apiServer.trim()); + config.key = config.key.trim(); + if (controllers != null) { + controllers[0].text = config.idServer; + controllers[1].text = config.relayServer; + controllers[2].text = config.apiServer; + controllers[3].text = config.key; + } + // id + if (config.idServer.isNotEmpty && errMsgs != null) { + errMsgs[0].value = translate(await bind.mainTestIfValidServer( + server: config.idServer, testWithProxy: true)); + if (errMsgs[0].isNotEmpty) { + return false; + } + } + // relay + if (config.relayServer.isNotEmpty && errMsgs != null) { + errMsgs[1].value = translate(await bind.mainTestIfValidServer( + server: config.relayServer, testWithProxy: true)); + if (errMsgs[1].isNotEmpty) { + return false; + } + } + // api + if (config.apiServer.isNotEmpty && errMsgs != null) { + if (!config.apiServer.startsWith('http://') && + !config.apiServer.startsWith('https://')) { + errMsgs[2].value = + '${translate("API Server")}: ${translate("invalid_http")}'; + return false; + } + } + final oldApiServer = await bind.mainGetApiServer(); + + // should set one by one + await bind.mainSetOption( + key: 'custom-rendezvous-server', value: config.idServer); + await bind.mainSetOption(key: 'relay-server', value: config.relayServer); + await bind.mainSetOption(key: 'api-server', value: config.apiServer); + await bind.mainSetOption(key: 'key', value: config.key); + final newApiServer = await bind.mainGetApiServer(); + if (oldApiServer.isNotEmpty && + oldApiServer != newApiServer && + gFFI.userModel.isLogin) { + gFFI.userModel.logOut(apiServer: oldApiServer); + } + return true; +} + +ColorFilter? svgColor(Color? color) { + if (color == null) { + return null; + } else { + return ColorFilter.mode(color, BlendMode.srcIn); + } +} + +// 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, + 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) { + index = 0; + } + var ref = values[index].obs; + current = keys[index]; + return Container( + decoration: BoxDecoration( + border: Border.all( + color: enabled + ? MyTheme.color(context).border2 ?? MyTheme.border + : MyTheme.border, + ), + borderRadius: + BorderRadius.circular(8), //border raiuds of dropdown button + ), + height: 42, // should be the height of a TextField + child: Obx(() => DropdownButton( + isExpanded: true, + value: ref.value, + elevation: 16, + underline: Container(), + style: TextStyle( + color: enabled + ? Theme.of(context).textTheme.titleMedium?.color + : disabledTextColor(context, enabled)), + icon: const Icon( + Icons.expand_more_sharp, + size: 20, + ).marginOnly(right: 15), + 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: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + ).marginOnly(left: 15), + ); + }).toList(), + )), + ).marginOnly(bottom: 5); + } +} + +Color? disabledTextColor(BuildContext context, bool enabled) { + return enabled + ? null + : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6); +} + +Widget loadPowered(BuildContext context) { + if (bind.mainGetBuildinOption(key: "hide-powered-by-me") == 'Y') { + return SizedBox.shrink(); + } + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + launchUrl(Uri.parse('https://rustdesk.com')); + }, + child: Opacity( + opacity: 0.5, + child: Text( + translate("powered_by_me"), + overflow: TextOverflow.clip, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(fontSize: 9, decoration: TextDecoration.underline), + )), + ), + ).marginOnly(top: 6); +} + +// max 300 x 60 +Widget loadLogo() { + return FutureBuilder( + future: rootBundle.load('assets/logo.png'), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + final image = Image.asset( + 'assets/logo.png', + fit: BoxFit.contain, + errorBuilder: (ctx, error, stackTrace) { + return Container(); + }, + ); + return Container( + constraints: BoxConstraints(maxWidth: 300, maxHeight: 60), + child: image, + ).marginOnly(left: 12, right: 12, top: 12); + } + return const Offstage(); + }); +} + +Widget loadIcon(double size) { + return Image.asset('assets/icon.png', + width: size, + height: size, + errorBuilder: (ctx, error, stackTrace) => SvgPicture.asset( + 'assets/icon.svg', + width: size, + height: size, + )); +} + +var imcomingOnlyHomeSize = Size(280, 300); +Size getIncomingOnlyHomeSize() { + final magicWidth = isWindows ? 11.0 : 2.0; + final magicHeight = 10.0; + return imcomingOnlyHomeSize + + Offset(magicWidth, kDesktopRemoteTabBarHeight + magicHeight); +} + +Size getIncomingOnlySettingsSize() { + return Size(768, 600); +} + +bool isInHomePage() { + final controller = Get.find(); + return controller.state.value.selected == 0; +} + +Widget _buildPresetPasswordWarning() { + if (bind.mainGetBuildinOption(key: kOptionRemovePresetPasswordWarning) != + 'N') { + return SizedBox.shrink(); + } + return Container( + color: Colors.yellow, + child: Column( + children: [ + Align( + child: Text( + translate("Security Alert"), + style: TextStyle( + color: Colors.red, + fontSize: + 18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261 + fontWeight: FontWeight.bold, + ), + )).paddingOnly(bottom: 8), + Text( + translate("preset_password_warning"), + style: TextStyle(color: Colors.red), + ) + ], + ).paddingAll(8), + ); // Show a warning message if the Future completed with true +} + +Widget buildPresetPasswordWarningMobile() { + if (bind.isPresetPasswordMobileOnly()) { + return _buildPresetPasswordWarning(); + } else { + return SizedBox.shrink(); + } +} + +Widget buildPresetPasswordWarning() { + return FutureBuilder( + future: bind.isPresetPassword(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete + } else if (snapshot.hasError) { + return Text( + 'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error + } else if (snapshot.hasData && snapshot.data == true) { + return _buildPresetPasswordWarning(); + } else { + return SizedBox + .shrink(); // Show nothing if the Future completed with false or null + } + }, + ); +} + +// https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44 +Widget buildVirtualWindowFrame(BuildContext context, Widget child) { + boxShadow() => isMainDesktopWindow + ? [ + if (stateGlobal.fullscreen.isFalse || stateGlobal.isMaximized.isFalse) + BoxShadow( + color: Colors.black.withOpacity(0.1), + offset: Offset( + 0.0, + stateGlobal.isFocused.isTrue + ? kFrameBoxShadowOffsetFocused + : kFrameBoxShadowOffsetUnfocused), + blurRadius: kFrameBoxShadowBlurRadius, + ), + ] + : null; + return Obx( + () => Container( + decoration: BoxDecoration( + color: isMainDesktopWindow + ? Colors.transparent + : Theme.of(context).colorScheme.background, + border: Border.all( + color: Theme.of(context).dividerColor, + width: stateGlobal.windowBorderWidth.value, + ), + borderRadius: BorderRadius.circular( + (stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue) + ? 0 + : kFrameBorderRadius, + ), + boxShadow: boxShadow(), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + (stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue) + ? 0 + : kFrameClipRRectBorderRadius, + ), + child: child, + ), + ), + ); +} + +get windowResizeEdgeSize => + isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize; + +// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable. +// See _linuxWindowResizable for more details. +// So we use `setResizable()` instead of `windowManager.setResizable()`. +// +// We can only call `windowManager.setResizable(false)` if we need the default size on Linux. +setResizable(bool resizable) { + if (isLinux) { + _linuxWindowResizable = resizable; + stateGlobal.refreshResizeEdgeSize(); + } else { + windowManager.setResizable(resizable); + } +} + +isOptionFixed(String key) => bind.mainIsOptionFixed(key: key); + +bool isChangePermanentPasswordDisabled() => + bind.mainGetBuildinOption(key: kOptionDisableChangePermanentPassword) == + 'Y'; + +bool isChangeIdDisabled() => + bind.mainGetBuildinOption(key: kOptionDisableChangeId) == 'Y'; + +bool isUnlockPinDisabled() => + bind.mainGetBuildinOption(key: kOptionDisableUnlockPin) == 'Y'; + +bool? _isCustomClient; +bool get isCustomClient { + _isCustomClient ??= bind.isCustomClient(); + return _isCustomClient!; +} + +get defaultOptionLang => isCustomClient ? 'default' : ''; +get defaultOptionTheme => isCustomClient ? 'system' : ''; +get defaultOptionYes => isCustomClient ? 'Y' : ''; +get defaultOptionNo => isCustomClient ? 'N' : ''; +get defaultOptionWhitelist => isCustomClient ? ',' : ''; +get defaultOptionAccessMode => isCustomClient ? 'custom' : ''; +get defaultOptionApproveMode => isCustomClient ? 'password-click' : ''; + +bool whitelistNotEmpty() { + // https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/#whitelist + final v = bind.mainGetOptionSync(key: kOptionWhitelist); + return v != '' && v != ','; +} + +// `setMovable()` is only supported on macOS. +// +// On macOS, the window can be dragged by the tab bar by default. +// We need to disable the movable feature to prevent the window from being dragged by the tabs in the tab bar. +// +// When we drag the blank tab bar (not the tab), the window will be dragged normally by adding the `onPanStart` handle. +// +// See the following code for more details: +// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L385 +// https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L399 +// +// @platforms macos +disableWindowMovable(int? windowId) { + if (!isMacOS) { + return; + } + + if (windowId == null) { + windowManager.setMovable(false); + } else { + WindowController.fromWindowId(windowId).setMovable(false); + } +} + +Widget netWorkErrorWidget() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(translate("network_error_tip")), + ElevatedButton( + onPressed: gFFI.userModel.refreshCurrentUser, + child: Text(translate("Retry"))) + .marginSymmetric(vertical: 16), + SelectableText(gFFI.userModel.networkError.value, + style: TextStyle(fontSize: 11, color: Colors.red)), + ], + )); +} + +List? get windowManagerEnableResizeEdges => isWindows + ? [ + ResizeEdge.topLeft, + ResizeEdge.top, + ResizeEdge.topRight, + ] + : null; + +List? get subWindowManagerEnableResizeEdges => isWindows + ? [ + SubWindowResizeEdge.topLeft, + SubWindowResizeEdge.top, + SubWindowResizeEdge.topRight, + ] + : null; + +void earlyAssert() { + assert('\1' == '1'); +} + +void checkUpdate() { + if (!isWeb) { + if (!bind.isCustomClient()) { + platformFFI.registerEventHandler( + kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, + (Map evt) async { + if (evt['url'] is String) { + stateGlobal.updateUrl.value = evt['url']; + } + }); + Timer(const Duration(seconds: 1), () async { + bind.mainGetSoftwareUpdateUrl(); + }); + } + } +} + +// https://github.com/flutter/flutter/issues/153560#issuecomment-2497160535 +// For TextField, TextFormField +extension WorkaroundFreezeLinuxMint on Widget { + Widget workaroundFreezeLinuxMint() { + // No need to check if is Linux Mint, because this workaround is harmless on other platforms. + if (isLinux) { + return ExcludeSemantics(child: this); + } else { + return this; + } + } +} + +// Don't use `extension` here, the border looks weird if using `extension` in my test. +Widget workaroundWindowBorder(BuildContext context, Widget child) { + if (!isWin10) { + return child; + } + + final isLight = Theme.of(context).brightness == Brightness.light; + final borderColor = isLight ? Colors.black87 : Colors.grey; + final width = isLight ? 0.5 : 0.1; + + getBorderWidget(Widget child) { + return Obx(() => + (stateGlobal.isMaximized.isTrue || stateGlobal.fullscreen.isTrue) + ? Offstage() + : child); + } + + final List borders = [ + getBorderWidget(Container( + color: borderColor, + height: width + 0.1, + )) + ]; + if (kWindowType == WindowType.Main && !isLight) { + borders.addAll([ + getBorderWidget(Align( + alignment: Alignment.topLeft, + child: Container( + color: borderColor, + width: width, + ), + )), + getBorderWidget(Align( + alignment: Alignment.topRight, + child: Container( + color: borderColor, + width: width, + ), + )), + getBorderWidget(Align( + alignment: Alignment.bottomCenter, + child: Container( + color: borderColor, + height: width, + ), + )), + ]); + } + return Stack( + children: [ + child, + ...borders, + ], + ); +} + +void updateTextAndPreserveSelection( + TextEditingController controller, String text) { + // Only care about select all for now. + final isSelected = controller.selection.isValid && + controller.selection.end > controller.selection.start; + + // Set text will make the selection invalid. + controller.text = text; + + if (isSelected) { + controller.selection = TextSelection( + baseOffset: 0, extentOffset: controller.value.text.length); + } +} + +List getPrinterNames() { + final printerNamesJson = bind.mainGetPrinterNames(); + if (printerNamesJson.isEmpty) { + return []; + } + try { + final List printerNamesList = jsonDecode(printerNamesJson); + final appPrinterName = '$appName Printer'; + return printerNamesList + .map((e) => e.toString()) + .where((name) => name != appPrinterName) + .toList(); + } catch (e) { + debugPrint('failed to parse printer names, err: $e'); + return []; + } +} + +String _appName = ''; +String get appName { + if (_appName.isEmpty) { + _appName = bind.mainGetAppNameSync(); + } + return _appName; +} + +String getConnectionText(bool secure, bool direct, String streamType) { + String connectionText; + if (secure && direct) { + connectionText = translate("Direct and encrypted connection"); + } else if (secure && !direct) { + connectionText = translate("Relayed and encrypted connection"); + } else if (!secure && direct) { + connectionText = translate("Direct and unencrypted connection"); + } else { + connectionText = translate("Relayed and unencrypted connection"); + } + if (streamType == 'Relay') { + streamType = 'TCP'; + } + if (streamType.isEmpty) { + return connectionText; + } else { + return '$connectionText ($streamType)'; + } +} + +String decode_http_response(http.Response resp) { + try { + // https://github.com/rustdesk/rustdesk-server-pro/discussions/758 + return utf8.decode(resp.bodyBytes, allowMalformed: true); + } catch (e) { + debugPrint('Failed to decode response as UTF-8: $e'); + // Fallback to bodyString which handles encoding automatically + return resp.body; + } +} + +bool peerTabShowNote(PeerTabIndex peerTabIndex) { + return peerTabIndex == PeerTabIndex.ab || peerTabIndex == PeerTabIndex.group; +} + +// TODO: We should support individual bits combinations in the future. +// But for now, just keep it simple, because the old code only supports single button. +// No users have requested multi-button support yet. +String mouseButtonsToPeer(int buttons) { + switch (buttons) { + case kPrimaryMouseButton: + return 'left'; + case kSecondaryMouseButton: + return 'right'; + case kMiddleMouseButton: + return 'wheel'; + case kBackMouseButton: + return 'back'; + case kForwardMouseButton: + return 'forward'; + default: + return ''; + } +} + +/// Build an avatar widget from an avatar URL or data URI string. +/// Returns [fallback] if avatar is empty or cannot be decoded. +/// [borderRadius] defaults to [size]/2 (circle). +Widget? buildAvatarWidget({ + required String avatar, + required double size, + double? borderRadius, + Widget? fallback, +}) { + final trimmed = avatar.trim(); + if (trimmed.isEmpty) return fallback; + + ImageProvider? imageProvider; + if (trimmed.startsWith('data:image/')) { + final comma = trimmed.indexOf(','); + if (comma > 0) { + try { + imageProvider = MemoryImage(base64Decode(trimmed.substring(comma + 1))); + } catch (_) {} + } + } else if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { + imageProvider = NetworkImage(trimmed); + } + + if (imageProvider == null) return fallback; + + final radius = borderRadius ?? size / 2; + return ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: Image( + image: imageProvider, + width: size, + height: size, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + fallback ?? SizedBox.shrink(), + ), + ); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/formatter/id_formatter.dart b/shelled/rustdesk-as-ref/flutter/lib/common/formatter/id_formatter.dart new file mode 100644 index 0000000..c2329d5 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/formatter/id_formatter.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class IDTextEditingController extends TextEditingController { + IDTextEditingController({String? text}) : super(text: text); + + String get id => trimID(value.text); + + set id(String newID) => text = formatID(newID); +} + +class IDTextInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + if (newValue.text.isEmpty) { + return newValue.copyWith(text: ''); + } else if (newValue.text.compareTo(oldValue.text) == 0) { + return newValue; + } else { + int selectionIndexFromTheRight = + newValue.text.length - newValue.selection.extentOffset; + String newID = formatID(newValue.text); + return TextEditingValue( + text: newID, + selection: TextSelection.collapsed( + offset: newID.length - selectionIndexFromTheRight, + ), + // https://github.com/flutter/flutter/issues/78066#issuecomment-797869906 + composing: newValue.composing, + ); + } + } +} + +String formatID(String id) { + String id2 = id.replaceAll(' ', ''); + String suffix = ''; + if (id2.endsWith(r'\r') || id2.endsWith(r'/r')) { + suffix = id2.substring(id2.length - 2, id2.length); + id2 = id2.substring(0, id2.length - 2); + } + if (int.tryParse(id2) == null) return id; + String newID = ''; + if (id2.length <= 3) { + newID = id2; + } else { + var n = id2.length; + var a = n % 3 != 0 ? n % 3 : 3; + newID = id2.substring(0, a); + for (var i = a; i < n; i += 3) { + newID += " ${id2.substring(i, i + 3)}"; + } + } + return newID + suffix; +} + +String trimID(String id) { + return id.replaceAll(' ', ''); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/hbbs/hbbs.dart b/shelled/rustdesk-as-ref/flutter/lib/common/hbbs/hbbs.dart new file mode 100644 index 0000000..0c729e4 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/hbbs/hbbs.dart @@ -0,0 +1,302 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; + +import 'package:flutter_hbb/models/peer_model.dart'; + +import '../../models/platform_model.dart'; + +class HttpType { + static const kAuthReqTypeAccount = "account"; + static const kAuthReqTypeMobile = "mobile"; + static const kAuthReqTypeSMSCode = "sms_code"; + static const kAuthReqTypeEmailCode = "email_code"; + static const kAuthReqTypeTfaCode = "tfa_code"; + + static const kAuthResTypeToken = "access_token"; + static const kAuthResTypeEmailCheck = "email_check"; + static const kAuthResTypeTfaCheck = "tfa_check"; +} + +enum UserStatus { kDisabled, kNormal, kUnverified } + +// to-do: The UserPayload does not contain all the fields of the user. +// Is all the fields of the user needed? +class UserPayload { + String name = ''; + String displayName = ''; + String avatar = ''; + String email = ''; + String note = ''; + String? verifier; + UserStatus status; + bool isAdmin = false; + + UserPayload.fromJson(Map json) + : name = json['name'] ?? '', + displayName = json['display_name'] ?? '', + avatar = json['avatar'] ?? '', + email = json['email'] ?? '', + note = json['note'] ?? '', + verifier = json['verifier'], + status = json['status'] == 0 + ? UserStatus.kDisabled + : json['status'] == -1 + ? UserStatus.kUnverified + : UserStatus.kNormal, + isAdmin = json['is_admin'] == true; + + Map toJson() { + final Map map = { + 'name': name, + 'display_name': displayName, + 'avatar': avatar, + 'status': status == UserStatus.kDisabled + ? 0 + : status == UserStatus.kUnverified + ? -1 + : 1, + }; + return map; + } + + Map toGroupCacheJson() { + final Map map = { + 'name': name, + 'display_name': displayName, + }; + return map; + } + + String get displayNameOrName { + return displayName.trim().isEmpty ? name : displayName; + } +} + +class PeerPayload { + String id = ''; + Map info = {}; + int? status; + String user = ''; + String user_name = ''; + String? device_group_name; + String note = ''; + + PeerPayload.fromJson(Map json) + : id = json['id'] ?? '', + info = (json['info'] is Map) ? json['info'] : {}, + status = json['status'], + user = json['user'] ?? '', + user_name = json['user_name'] ?? '', + device_group_name = json['device_group_name'] ?? '', + note = json['note'] ?? ''; + + static Peer toPeer(PeerPayload p) { + return Peer.fromJson({ + "id": p.id, + 'loginName': p.user_name, + "username": p.info['username'] ?? '', + "platform": _platform(p.info['os']), + "hostname": p.info['device_name'], + "device_group_name": p.device_group_name, + "note": p.note, + }); + } + + static String? _platform(dynamic field) { + if (field == null) { + return null; + } + final fieldStr = field.toString(); + List list = fieldStr.split(' / '); + if (list.isEmpty) return null; + final os = list[0]; + switch (os.toLowerCase()) { + case 'windows': + return kPeerPlatformWindows; + case 'linux': + return kPeerPlatformLinux; + case 'macos': + return kPeerPlatformMacOS; + case 'android': + return kPeerPlatformAndroid; + default: + if (fieldStr.toLowerCase().contains('linux')) { + return kPeerPlatformLinux; + } + return null; + } + } +} + +class LoginRequest { + String? username; + String? password; + String? id; + String? uuid; + bool? autoLogin; + String? type; + String? verificationCode; + String? tfaCode; + String? secret; + + LoginRequest( + {this.username, + this.password, + this.id, + this.uuid, + this.autoLogin, + this.type, + this.verificationCode, + this.tfaCode, + this.secret}); + + Map toJson() { + final Map data = {}; + if (username != null) data['username'] = username; + if (password != null) data['password'] = password; + if (id != null) data['id'] = id; + if (uuid != null) data['uuid'] = uuid; + if (autoLogin != null) data['autoLogin'] = autoLogin; + if (type != null) data['type'] = type; + if (verificationCode != null) { + data['verificationCode'] = verificationCode; + } + if (tfaCode != null) data['tfaCode'] = tfaCode; + if (secret != null) data['secret'] = secret; + + Map deviceInfo = {}; + try { + deviceInfo = jsonDecode(bind.mainGetLoginDeviceInfo()); + } catch (e) { + debugPrint('Failed to decode get device info: $e'); + } + data['deviceInfo'] = deviceInfo; + return data; + } +} + +class LoginResponse { + String? access_token; + String? type; + String? tfa_type; + String? secret; + UserPayload? user; + + LoginResponse( + {this.access_token, this.type, this.tfa_type, this.secret, this.user}); + + LoginResponse.fromJson(Map json) { + access_token = json['access_token']; + type = json['type']; + tfa_type = json['tfa_type']; + secret = json['secret']; + user = json['user'] != null ? UserPayload.fromJson(json['user']) : null; + } +} + +class RequestException implements Exception { + int statusCode; + String cause; + RequestException(this.statusCode, this.cause); + + @override + String toString() { + return "RequestException, statusCode: $statusCode, error: $cause"; + } +} + +enum ShareRule { + read(1), + readWrite(2), + fullControl(3); + + const ShareRule(this.value); + final int value; + + static String desc(int v) { + if (v == ShareRule.read.value) { + return translate('Read-only'); + } + if (v == ShareRule.readWrite.value) { + return translate('Read/Write'); + } + if (v == ShareRule.fullControl.value) { + return translate('Full Control'); + } + return v.toString(); + } + + static String shortDesc(int v) { + if (v == ShareRule.read.value) { + return 'R'; + } + if (v == ShareRule.readWrite.value) { + return 'RW'; + } + if (v == ShareRule.fullControl.value) { + return 'F'; + } + return v.toString(); + } + + static ShareRule? fromValue(int v) { + if (v == ShareRule.read.value) { + return ShareRule.read; + } + if (v == ShareRule.readWrite.value) { + return ShareRule.readWrite; + } + if (v == ShareRule.fullControl.value) { + return ShareRule.fullControl; + } + return null; + } +} + +class AbProfile { + String guid; + String name; + String owner; + String? note; + dynamic info; + int rule; + + AbProfile(this.guid, this.name, this.owner, this.note, this.rule, this.info); + + AbProfile.fromJson(Map json) + : guid = json['guid'] ?? '', + name = json['name'] ?? '', + owner = json['owner'] ?? '', + note = json['note'] ?? '', + info = json['info'], + rule = json['rule'] ?? 0; +} + +class AbTag { + String name; + int color; + + AbTag(this.name, this.color); + + AbTag.fromJson(Map json) + : name = json['name'] ?? '', + color = json['color'] ?? ''; +} + +class DeviceGroupPayload { + String name; + + DeviceGroupPayload(this.name); + + DeviceGroupPayload.fromJson(Map json) + : name = json['name'] ?? ''; + + Map toGroupCacheJson() { + final Map map = { + 'name': name, + }; + return map; + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/shared_state.dart b/shelled/rustdesk-as-ref/flutter/lib/common/shared_state.dart new file mode 100644 index 0000000..4f9373c --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/shared_state.dart @@ -0,0 +1,368 @@ +import 'package:flutter_hbb/common.dart'; +import 'package:get/get.dart'; + +import '../consts.dart'; + +// TODO: A lot of dup code. + +class PrivacyModeState { + static String tag(String id) => 'privacy_mode_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxString state = ''.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } else { + Get.find(tag: key).value = ''; + } + } + + static RxString find(String id) => Get.find(tag: tag(id)); +} + +class BlockInputState { + static String tag(String id) => 'block_input_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = false; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class CurrentDisplayState { + static String tag(String id) => 'current_display_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxInt state = RxInt(0); + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = 0; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxInt find(String id) => Get.find(tag: tag(id)); +} + +class ConnectionType { + final Rx _secure = kInvalidValueStr.obs; + final Rx _direct = kInvalidValueStr.obs; + final Rx _stream_type = kInvalidValueStr.obs; + + Rx get secure => _secure; + Rx get direct => _direct; + Rx get stream_type => _stream_type; + + static String get strSecure => 'secure'; + static String get strInsecure => 'insecure'; + static String get strDirect => ''; + static String get strIndirect => '_relay'; + + void setSecure(bool v) { + _secure.value = v ? strSecure : strInsecure; + } + + void setDirect(bool v) { + _direct.value = v ? strDirect : strIndirect; + } + + void setStreamType(String v) { + _stream_type.value = v; + } + + bool isValid() { + return _secure.value != kInvalidValueStr && + _direct.value != kInvalidValueStr && + _stream_type.value != kInvalidValueStr; + } +} + +class ConnectionTypeState { + static String tag(String id) => 'connection_type_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final ConnectionType collectionType = ConnectionType(); + Get.put(collectionType, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static ConnectionType find(String id) => + Get.find(tag: tag(id)); +} + +class FingerprintState { + static String tag(String id) => 'fingerprint_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxString state = ''.obs; + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = ''; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxString find(String id) => Get.find(tag: tag(id)); +} + +class ShowRemoteCursorState { + static String tag(String id) => 'show_remote_cursor_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = false; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class ShowRemoteCursorLockState { + static String tag(String id) => 'show_remote_cursor_lock_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = false; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class KeyboardEnabledState { + static String tag(String id) => 'keyboard_enabled_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + // Server side, default true + final RxBool state = true.obs; + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = true; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class RemoteCursorMovedState { + static String tag(String id) => 'remote_cursor_moved_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = false; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class RemoteCountState { + static String tag() => 'remote_count_'; + + static void init() { + final key = tag(); + if (!Get.isRegistered(tag: key)) { + final RxInt state = 1.obs; + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = 1; + } + } + + static void delete() { + final key = tag(); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxInt find() => Get.find(tag: tag()); +} + +class PeerBoolOption { + static String tag(String id, String opt) => 'peer_{$opt}_$id'; + + static void init(String id, String opt, bool Function() init_getter) { + final key = tag(id, opt); + if (!Get.isRegistered(tag: key)) { + final RxBool value = RxBool(init_getter()); + Get.put(value, tag: key); + } else { + Get.find(tag: key).value = init_getter(); + } + } + + static void delete(String id, String opt) { + final key = tag(id, opt); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id, String opt) => + Get.find(tag: tag(id, opt)); +} + +class PeerStringOption { + static String tag(String id, String opt) => 'peer_{$opt}_$id'; + + static void init(String id, String opt, String Function() init_getter) { + final key = tag(id, opt); + if (!Get.isRegistered(tag: key)) { + final RxString value = RxString(init_getter()); + Get.put(value, tag: key); + } else { + Get.find(tag: key).value = init_getter(); + } + } + + static void delete(String id, String opt) { + final key = tag(id, opt); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxString find(String id, String opt) => + Get.find(tag: tag(id, opt)); +} + +class UnreadChatCountState { + static String tag(id) => 'unread_chat_count_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxInt state = RxInt(0); + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = 0; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxInt find(String id) => Get.find(tag: tag(id)); +} + +initSharedStates(String id) { + PrivacyModeState.init(id); + BlockInputState.init(id); + CurrentDisplayState.init(id); + KeyboardEnabledState.init(id); + ShowRemoteCursorState.init(id); + ShowRemoteCursorLockState.init(id); + RemoteCursorMovedState.init(id); + FingerprintState.init(id); + PeerBoolOption.init(id, kOptionZoomCursor, () => false); + UnreadChatCountState.init(id); + if (isMobile) ConnectionTypeState.init(id); // desktop in other places +} + +removeSharedStates(String id) { + PrivacyModeState.delete(id); + BlockInputState.delete(id); + CurrentDisplayState.delete(id); + ShowRemoteCursorState.delete(id); + ShowRemoteCursorLockState.delete(id); + KeyboardEnabledState.delete(id); + RemoteCursorMovedState.delete(id); + FingerprintState.delete(id); + PeerBoolOption.delete(id, kOptionZoomCursor); + UnreadChatCountState.delete(id); + if (isMobile) ConnectionTypeState.delete(id); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/address_book.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/address_book.dart new file mode 100644 index 0000000..1a09d6f --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/address_book.dart @@ -0,0 +1,899 @@ +import 'dart:math'; + +import 'package:bot_toast/bot_toast.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:dynamic_layouts/dynamic_layouts.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/formatter/id_formatter.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/common/widgets/peer_card.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/models/ab_model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; +import 'package:get/get.dart'; +import 'package:flex_color_picker/flex_color_picker.dart'; + +import '../../common.dart'; +import 'dialog.dart'; +import 'login.dart'; + +final hideAbTagsPanel = false.obs; + +class AddressBook extends StatefulWidget { + final EdgeInsets? menuPadding; + const AddressBook({Key? key, this.menuPadding}) : super(key: key); + + @override + State createState() { + return _AddressBookState(); + } +} + +class _AddressBookState extends State { + var menuPos = RelativeRect.fill; + + @override + Widget build(BuildContext context) => Obx(() { + if (!gFFI.userModel.isLogin) { + return Center( + child: ElevatedButton( + onPressed: loginDialog, child: Text(translate("Login")))); + } else if (gFFI.userModel.networkError.isNotEmpty) { + return netWorkErrorWidget(); + } else { + return Column( + children: [ + // NOT use Offstage to wrap LinearProgressIndicator + if (gFFI.abModel.currentAbLoading.value && + gFFI.abModel.currentAbEmpty) + const LinearProgressIndicator(), + buildErrorBanner(context, + loading: gFFI.abModel.currentAbLoading, + err: gFFI.abModel.currentAbPullError, + retry: null, + close: () => gFFI.abModel.currentAbPullError.value = ''), + buildErrorBanner(context, + loading: gFFI.abModel.currentAbLoading, + err: gFFI.abModel.currentAbPushError, + retry: null, // remove retry + close: () => gFFI.abModel.currentAbPushError.value = ''), + Expanded( + child: Obx(() => stateGlobal.isPortrait.isTrue + ? _buildAddressBookPortrait() + : _buildAddressBookLandscape()), + ), + ], + ); + } + }); + + Widget _buildAddressBookLandscape() { + return Row( + children: [ + Offstage( + offstage: hideAbTagsPanel.value, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of(context).colorScheme.background)), + child: Container( + width: 200, + height: double.infinity, + child: Column( + children: [ + _buildAbDropdown(), + _buildTagHeader().marginOnly( + left: 8.0, + right: gFFI.abModel.legacyMode.value ? 8.0 : 0, + top: gFFI.abModel.legacyMode.value ? 8.0 : 0), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + child: _buildTags(), + ), + ), + _buildAbPermission(), + ], + ), + ), + ).marginOnly(right: 12.0)), + _buildPeersViews() + ], + ); + } + + Widget _buildAddressBookPortrait() { + const padding = 8.0; + return Column( + children: [ + Offstage( + offstage: hideAbTagsPanel.value, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: Theme.of(context).colorScheme.background)), + child: Container( + padding: + const EdgeInsets.fromLTRB(padding, 0, padding, padding), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildAbDropdown(), + _buildTagHeader().marginOnly(left: 8.0, right: 0), + Container( + width: double.infinity, + child: _buildTags(), + ), + ], + ), + ), + ).marginOnly(bottom: 12.0)), + _buildPeersViews() + ], + ); + } + + Widget _buildAbPermission() { + icon(IconData data, String tooltip) { + return Tooltip( + message: translate(tooltip), + waitDuration: Duration.zero, + child: Icon(data, size: 12.0).marginSymmetric(horizontal: 2.0)); + } + + return Obx(() { + if (gFFI.abModel.legacyMode.value) return Offstage(); + if (gFFI.abModel.current.isPersonal()) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + icon(Icons.cloud_off, "Personal"), + ], + ); + } else { + List children = []; + final rule = gFFI.abModel.current.sharedProfile()?.rule; + if (rule == ShareRule.read.value) { + children.add( + icon(Icons.visibility, ShareRule.desc(ShareRule.read.value))); + } else if (rule == ShareRule.readWrite.value) { + children + .add(icon(Icons.edit, ShareRule.desc(ShareRule.readWrite.value))); + } else if (rule == ShareRule.fullControl.value) { + children.add(icon( + Icons.security, ShareRule.desc(ShareRule.fullControl.value))); + } + final owner = gFFI.abModel.current.sharedProfile()?.owner; + if (owner != null) { + children.add(icon(Icons.person, "${translate("Owner")}: $owner")); + } + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: children, + ); + } + }); + } + + Widget _buildAbDropdown() { + if (gFFI.abModel.legacyMode.value) { + return Offstage(); + } + final names = gFFI.abModel.addressBookNames(); + if (!names.contains(gFFI.abModel.currentName.value)) { + return Offstage(); + } + // order: personal, divider, character order + // https://pub.dev/packages/dropdown_button2#3-dropdownbutton2-with-items-of-different-heights-like-dividers + final personalAddressBookName = gFFI.abModel.personalAddressBookName(); + bool contains = names.remove(personalAddressBookName); + names.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); + if (contains) { + names.insert(0, personalAddressBookName); + } + + Row buildItem(String e, {bool button = false}) { + return Row( + children: [ + Expanded( + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: gFFI.abModel.translatedName(e), + child: Text( + gFFI.abModel.translatedName(e), + style: button ? null : TextStyle(fontSize: 14.0), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: button ? TextAlign.center : null, + )), + ), + ], + ); + } + + final items = names + .map((e) => DropdownMenuItem(value: e, child: buildItem(e))) + .toList(); + var menuItemStyleData = MenuItemStyleData(height: 36); + if (contains && items.length > 1) { + items.insert(1, DropdownMenuItem(enabled: false, child: Divider())); + List customHeights = List.filled(items.length, 36); + customHeights[1] = 4; + menuItemStyleData = MenuItemStyleData(customHeights: customHeights); + } + final TextEditingController textEditingController = TextEditingController(); + + final isOptFixed = isOptionFixed(kOptionCurrentAbName); + return DropdownButton2( + value: gFFI.abModel.currentName.value, + onChanged: isOptFixed + ? null + : (value) { + if (value != null) { + gFFI.abModel.setCurrentName(value); + bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value); + } + }, + customButton: Obx(() => Container( + height: stateGlobal.isPortrait.isFalse ? 48 : 40, + child: Row(children: [ + Expanded( + child: + buildItem(gFFI.abModel.currentName.value, button: true)), + Icon(Icons.arrow_drop_down), + ]), + )), + underline: Container( + height: 0.7, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + menuItemStyleData: menuItemStyleData, + items: items, + isExpanded: true, + isDense: true, + dropdownSearchData: DropdownSearchData( + searchController: textEditingController, + searchInnerWidgetHeight: 50, + searchInnerWidget: Container( + height: 50, + padding: const EdgeInsets.only( + top: 8, + bottom: 4, + right: 8, + left: 8, + ), + child: TextFormField( + expands: true, + maxLines: null, + controller: textEditingController, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + hintText: translate('Search'), + hintStyle: const TextStyle(fontSize: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ).workaroundFreezeLinuxMint(), + ), + searchMatchFn: (item, searchValue) { + return item.value + .toString() + .toLowerCase() + .contains(searchValue.toLowerCase()); + }, + ), + ); + } + + Widget _buildTagHeader() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(translate('Tags')), + Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onPointerUp: (_) => _showMenu(menuPos), + child: build_more(context, invert: true)), + ], + ); + } + + Widget _buildTags() { + return Obx(() { + List tags; + if (gFFI.abModel.sortTags.value) { + tags = gFFI.abModel.currentAbTags.toList(); + tags.sort(); + } else { + tags = gFFI.abModel.currentAbTags.toList(); + } + tags = [kUntagged, ...tags].toList(); + final editPermission = gFFI.abModel.current.canWrite(); + tagBuilder(String e) { + return AddressBookTag( + name: e, + tags: gFFI.abModel.selectedTags, + onTap: () { + if (gFFI.abModel.selectedTags.contains(e)) { + gFFI.abModel.selectedTags.remove(e); + } else { + gFFI.abModel.selectedTags.add(e); + } + }, + showActionMenu: editPermission); + } + + gridView(bool isPortrait) => DynamicGridView.builder( + shrinkWrap: isPortrait, + gridDelegate: SliverGridDelegateWithWrapping(), + itemCount: tags.length, + itemBuilder: (BuildContext context, int index) { + final e = tags[index]; + return tagBuilder(e); + }); + final maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); + return Obx(() => stateGlobal.isPortrait.isFalse + ? gridView(false) + : LimitedBox(maxHeight: maxHeight, child: gridView(true))); + }); + } + + Widget _buildPeersViews() { + return Expanded( + child: Align( + alignment: Alignment.topLeft, + child: AddressBookPeersView( + menuPadding: widget.menuPadding, + )), + ); + } + + @protected + MenuEntryBase syncMenuItem() { + final isOptFixed = isOptionFixed(syncAbOption); + return MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate('Sync with recent sessions'), + getter: () async { + return shouldSyncAb(); + }, + setter: (bool v) async { + gFFI.abModel.setShouldAsync(v); + }, + dismissOnClicked: true, + enabled: (!isOptFixed).obs, + ); + } + + @protected + MenuEntryBase sortMenuItem() { + final isOptFixed = isOptionFixed(sortAbTagsOption); + return MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate('Sort tags'), + getter: () async { + return shouldSortTags(); + }, + setter: (bool v) async { + bind.mainSetLocalOption( + key: sortAbTagsOption, value: v ? 'Y' : defaultOptionNo); + gFFI.abModel.sortTags.value = v; + }, + dismissOnClicked: true, + enabled: (!isOptFixed).obs, + ); + } + + @protected + MenuEntryBase filterMenuItem() { + final isOptFixed = isOptionFixed(filterAbTagOption); + return MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate('Filter by intersection'), + getter: () async { + return filterAbTagByIntersection(); + }, + setter: (bool v) async { + bind.mainSetLocalOption( + key: filterAbTagOption, value: v ? 'Y' : defaultOptionNo); + gFFI.abModel.filterByIntersection.value = v; + }, + dismissOnClicked: true, + enabled: (!isOptFixed).obs, + ); + } + + void _showMenu(RelativeRect pos) { + final canWrite = gFFI.abModel.current.canWrite(); + final items = [ + if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb), + if (canWrite) getEntry(translate("Add Tag"), abAddTag), + getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags), + if (gFFI.abModel.legacyMode.value) + sortMenuItem(), // It's already sorted after pulling down + if (canWrite) syncMenuItem(), + filterMenuItem(), + if (!gFFI.abModel.legacyMode.value && canWrite) + MenuEntryDivider(), + if (!gFFI.abModel.legacyMode.value && canWrite) + getEntry(translate("ab_web_console_tip"), () async { + final url = await bind.mainGetApiServer(); + if (await canLaunchUrlString(url)) { + launchUrlString(url); + } + }), + ]; + + mod_menu.showMenu( + context: context, + position: pos, + items: items + .map((e) => e.build( + context, + MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, + ); + } + + void addIdToCurrentAb() async { + if (gFFI.abModel.isCurrentAbFull(true)) { + return; + } + var isInProgress = false; + var passwordVisible = false; + IDTextEditingController idController = IDTextEditingController(text: ''); + TextEditingController aliasController = TextEditingController(text: ''); + TextEditingController passwordController = TextEditingController(text: ''); + TextEditingController noteController = TextEditingController(text: ''); + final tags = List.of(gFFI.abModel.currentAbTags); + var selectedTag = List.empty(growable: true).obs; + final style = TextStyle(fontSize: 14.0); + String? errorMsg; + final isCurrentAbShared = !gFFI.abModel.current.isPersonal(); + + gFFI.dialogManager.show((setState, close, context) { + submit() async { + setState(() { + isInProgress = true; + errorMsg = null; + }); + String id = idController.id; + if (id.isEmpty) { + // pass + } else { + if (gFFI.abModel.idContainByCurrent(id)) { + setState(() { + isInProgress = false; + errorMsg = translate('ID already exists'); + }); + return; + } + var password = ''; + if (isCurrentAbShared) { + password = passwordController.text; + } + String? errMsg2 = await gFFI.abModel.addIdToCurrent( + id, + aliasController.text.trim(), + password, + selectedTag, + noteController.text); + if (errMsg2 != null) { + setState(() { + isInProgress = false; + errorMsg = errMsg2; + }); + return; + } + // final currentPeers + } + close(); + } + + double marginBottom = 4; + + row({required Widget label, required Widget input}) { + makeChild(bool isPortrait) => Row( + children: [ + !isPortrait + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: label.marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 200), + child: input), + ), + ], + ).marginOnly(bottom: !isPortrait ? 8 : 0); + return Obx(() => makeChild(stateGlobal.isPortrait.isTrue)); + } + + return CustomAlertDialog( + title: Text(translate("Add ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + row( + label: Row( + children: [ + Text( + '*', + style: TextStyle(color: Colors.red, fontSize: 14), + ), + Text( + 'ID', + style: style, + ), + ], + ), + input: Obx(() => TextField( + controller: idController, + inputFormatters: [IDTextInputFormatter()], + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('ID'), + errorText: errorMsg, + errorMaxLines: 5), + ).workaroundFreezeLinuxMint())), + row( + label: Text( + translate('Alias'), + style: style, + ), + input: Obx(() => TextField( + controller: aliasController, + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('Alias'), + ), + ).workaroundFreezeLinuxMint()), + ), + if (isCurrentAbShared) + row( + label: Text( + translate('Password'), + style: style, + ), + input: Obx( + () => TextField( + controller: passwordController, + obscureText: !passwordVisible, + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('Password'), + suffixIcon: IconButton( + icon: Icon( + passwordVisible + ? Icons.visibility + : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), + onPressed: () { + setState(() { + passwordVisible = !passwordVisible; + }); + }, + ), + ), + ).workaroundFreezeLinuxMint(), + )), + row( + label: Text( + translate('Note'), + style: style, + ), + input: Obx( + () => TextField( + controller: noteController, + maxLines: 3, + minLines: 1, + maxLength: 300, + decoration: InputDecoration( + labelText: stateGlobal.isPortrait.isFalse + ? null + : translate('Note'), + ), + ).workaroundFreezeLinuxMint(), + )), + if (gFFI.abModel.currentAbTags.isNotEmpty) + Align( + alignment: Alignment.centerLeft, + child: Text( + translate('Tags'), + style: style, + ), + ).marginOnly(top: 8, bottom: marginBottom), + if (gFFI.abModel.currentAbTags.isNotEmpty) + Align( + alignment: Alignment.centerLeft, + child: Wrap( + children: tags + .map((e) => AddressBookTag( + name: e, + tags: selectedTag, + onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + }, + showActionMenu: false)) + .toList(growable: false), + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + if (!gFFI.abModel.current.isPersonal()) + Row(children: [ + Icon(Icons.info, color: Colors.amber).marginOnly(right: 4), + Text( + translate('share_warning_tip'), + style: TextStyle(fontSize: 12), + ) + ]).marginSymmetric(vertical: 10), + // NOT use Offstage to wrap LinearProgressIndicator + if (isInProgress) const LinearProgressIndicator(), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + void abAddTag() async { + var field = ""; + var msg = ""; + var isInProgress = false; + TextEditingController controller = TextEditingController(text: field); + gFFI.dialogManager.show((setState, close, context) { + submit() async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = controller.text.trim(); + if (field.isEmpty) { + // pass + } else { + final tags = field.trim().split(RegExp(r"[\s,;\n]+")); + field = tags.join(','); + for (var t in [kUntagged, translate(kUntagged)]) { + if (tags.contains(t)) { + BotToast.showText( + contentColor: Colors.red, text: 'Tag name cannot be "$t"'); + isInProgress = false; + return; + } + } + gFFI.abModel.addTags(tags); + // final currentPeers + } + close(); + } + + return CustomAlertDialog( + title: Text(translate("Add Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + decoration: InputDecoration( + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + autofocus: true, + ).workaroundFreezeLinuxMint(), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + // NOT use Offstage to wrap LinearProgressIndicator + if (isInProgress) const LinearProgressIndicator(), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } +} + +class AddressBookTag extends StatelessWidget { + final String name; + final RxList tags; + final Function()? onTap; + final bool showActionMenu; + + const AddressBookTag( + {Key? key, + required this.name, + required this.tags, + this.onTap, + this.showActionMenu = true}) + : super(key: key); + + @override + Widget build(BuildContext context) { + var pos = RelativeRect.fill; + + void setPosition(TapDownDetails e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + pos = RelativeRect.fromLTRB(x, y, x, y); + } + + const double radius = 8; + final isUnTagged = name == kUntagged; + final showAction = showActionMenu && !isUnTagged; + return GestureDetector( + onTap: onTap, + onTapDown: showAction ? setPosition : null, + onSecondaryTapDown: showAction ? setPosition : null, + onSecondaryTap: showAction ? () => _showMenu(context, pos) : null, + onLongPress: showAction ? () => _showMenu(context, pos) : null, + child: Obx(() => Container( + decoration: BoxDecoration( + color: tags.contains(name) + ? gFFI.abModel.getCurrentAbTagColor(name) + : Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(4)), + margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0), + child: IntrinsicWidth( + child: Row( + children: [ + if (!isUnTagged) + Container( + width: radius, + height: radius, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: tags.contains(name) + ? Colors.white + : gFFI.abModel.getCurrentAbTagColor(name)), + ).marginOnly(right: radius / 2), + Expanded( + child: Text(isUnTagged ? translate(name) : name, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: tags.contains(name) ? Colors.white : null)), + ), + ], + ), + ), + )), + ); + } + + void _showMenu(BuildContext context, RelativeRect pos) { + final items = [ + getEntry(translate("Rename"), () { + renameDialog( + oldName: name, + validator: (String? newName) { + if (newName == null || newName.isEmpty) { + return translate('Can not be empty'); + } + if (newName != name && + gFFI.abModel.currentAbTags.contains(newName)) { + return translate('Already exists'); + } + return null; + }, + onSubmit: (String newName) { + if (name != newName) { + gFFI.abModel.renameTag(name, newName); + } + Future.delayed(Duration.zero, () => Get.back()); + }, + onCancel: () { + Future.delayed(Duration.zero, () => Get.back()); + }); + }), + getEntry(translate(translate('Change Color')), () async { + final model = gFFI.abModel; + Color oldColor = model.getCurrentAbTagColor(name); + Color newColor = await showColorPickerDialog( + context, + oldColor, + pickersEnabled: { + ColorPickerType.accent: false, + ColorPickerType.wheel: true, + }, + pickerTypeLabels: { + ColorPickerType.primary: translate("Primary Color"), + ColorPickerType.wheel: translate("HSV Color"), + }, + actionButtons: ColorPickerActionButtons( + dialogOkButtonLabel: translate("OK"), + dialogCancelButtonLabel: translate("Cancel")), + showColorCode: true, + ); + if (oldColor != newColor) { + model.setTagColor(name, newColor); + } + }), + getEntry(translate("Delete"), () { + gFFI.abModel.deleteTag(name); + Future.delayed(Duration.zero, () => Get.back()); + }), + ]; + + mod_menu.showMenu( + context: context, + position: pos, + items: items + .map((e) => e.build( + context, + MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, + ); + } +} + +MenuEntryButton getEntry(String title, VoidCallback proc) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + title, + style: style, + ), + proc: proc, + dismissOnClicked: true, + ); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/animated_rotation_widget.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/animated_rotation_widget.dart new file mode 100644 index 0000000..0efc715 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/animated_rotation_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class AnimatedRotationWidget extends StatefulWidget { + final VoidCallback onPressed; + final ValueChanged? onHover; + final Widget child; + final RxBool? spinning; + const AnimatedRotationWidget( + {super.key, + required this.onPressed, + required this.child, + this.spinning, + this.onHover}); + + @override + State createState() => AnimatedRotationWidgetState(); +} + +class AnimatedRotationWidgetState extends State { + double turns = 0.0; + + @override + void initState() { + super.initState(); + widget.spinning?.listen((v) { + if (v && mounted) { + setState(() { + turns += 1; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedRotation( + turns: turns, + duration: const Duration(milliseconds: 200), + onEnd: () { + if (widget.spinning?.value == true && mounted) { + setState(() => turns += 1.0); + } + }, + child: InkWell( + onTap: () { + if (mounted) setState(() => turns += 1.0); + widget.onPressed(); + }, + onHover: widget.onHover, + child: widget.child)); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/audio_input.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/audio_input.dart new file mode 100644 index 0000000..1f8f1a8 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/audio_input.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; + +const _kSystemSound = 'System Sound'; + +typedef AudioINputSetDevice = void Function(String device); +typedef AudioInputBuilder = Widget Function( + List devices, String currentDevice, AudioINputSetDevice setDevice); + +class AudioInput extends StatelessWidget { + final AudioInputBuilder builder; + final bool isCm; + final bool isVoiceCall; + + const AudioInput( + {Key? key, + required this.builder, + required this.isCm, + required this.isVoiceCall}) + : super(key: key); + + static String getDefault() { + if (bind.mainAudioSupportLoopback()) return translate(_kSystemSound); + return ''; + } + + static Future getAudioInput(bool isCm, bool isVoiceCall) { + if (isVoiceCall) { + return bind.getVoiceCallInputDevice(isCm: isCm); + } else { + return bind.mainGetOption(key: 'audio-input'); + } + } + + static Future getValue(bool isCm, bool isVoiceCall) async { + String device = await getAudioInput(isCm, isVoiceCall); + if (device.isNotEmpty) { + return device; + } else { + return getDefault(); + } + } + + static Future setDevice( + String device, bool isCm, bool isVoiceCall) async { + if (device == getDefault()) device = ''; + if (isVoiceCall) { + await bind.setVoiceCallInputDevice(isCm: isCm, device: device); + } else { + await bind.mainSetOption(key: 'audio-input', value: device); + } + } + + static Future> getDevicesInfo( + bool isCm, bool isVoiceCall) async { + List devices = (await bind.mainGetSoundInputs()).toList(); + if (bind.mainAudioSupportLoopback()) { + devices.insert(0, translate(_kSystemSound)); + } + String current = await getValue(isCm, isVoiceCall); + return {'devices': devices, 'current': current}; + } + + @override + Widget build(BuildContext context) { + return futureBuilder( + future: getDevicesInfo(isCm, isVoiceCall), + hasData: (data) { + String currentDevice = data['current']; + List devices = data['devices'] as List; + if (devices.isEmpty) { + return const Offstage(); + } + return builder(devices, currentDevice, (devices) { + setDevice(devices, isCm, isVoiceCall); + }); + }, + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/autocomplete.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/autocomplete.dart new file mode 100644 index 0000000..ec64cca --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/autocomplete.dart @@ -0,0 +1,257 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/formatter/id_formatter.dart'; +import '../../../models/platform_model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/peer_card.dart'; + +class AllPeersLoader { + List peers = []; + + bool _isPeersLoading = false; + bool _isPeersLoaded = false; + + final String _listenerKey = 'AllPeersLoader'; + + late void Function(VoidCallback) setState; + + bool get needLoad => !_isPeersLoaded && !_isPeersLoading; + bool get isPeersLoaded => _isPeersLoaded; + + AllPeersLoader(); + + void init(void Function(VoidCallback) setState) { + this.setState = setState; + gFFI.recentPeersModel.addListener(_mergeAllPeers); + gFFI.lanPeersModel.addListener(_mergeAllPeers); + gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); + gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); + } + + void clear() { + gFFI.recentPeersModel.removeListener(_mergeAllPeers); + gFFI.lanPeersModel.removeListener(_mergeAllPeers); + gFFI.abModel.removePeerUpdateListener(_listenerKey); + gFFI.groupModel.removePeerUpdateListener(_listenerKey); + } + + Future getAllPeers() async { + if (!needLoad) { + return; + } + _isPeersLoading = true; + + if (gFFI.recentPeersModel.peers.isEmpty) { + bind.mainLoadRecentPeers(); + } + if (gFFI.lanPeersModel.peers.isEmpty) { + bind.mainLoadLanPeers(); + } + // No need to care about peers from abModel, and group model. + // Because they will pull data in `refreshCurrentUser()` on startup. + + final startTime = DateTime.now(); + _mergeAllPeers(); + final diffTime = DateTime.now().difference(startTime).inMilliseconds; + if (diffTime < 100) { + await Future.delayed(Duration(milliseconds: diffTime)); + } + } + + void _mergeAllPeers() { + Map combinedPeers = {}; + for (var p in gFFI.abModel.allPeers()) { + if (!combinedPeers.containsKey(p.id)) { + combinedPeers[p.id] = p.toJson(); + } + } + for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) { + if (!combinedPeers.containsKey(p.id)) { + combinedPeers[p.id] = p.toJson(); + } + } + + List parsedPeers = []; + for (var peer in combinedPeers.values) { + parsedPeers.add(Peer.fromJson(peer)); + } + + Set peerIds = combinedPeers.keys.toSet(); + for (final peer in gFFI.lanPeersModel.peers) { + if (!peerIds.contains(peer.id)) { + parsedPeers.add(peer); + peerIds.add(peer.id); + } + } + + for (final peer in gFFI.recentPeersModel.peers) { + if (!peerIds.contains(peer.id)) { + parsedPeers.add(peer); + peerIds.add(peer.id); + } + } + for (final id in gFFI.recentPeersModel.restPeerIds) { + if (!peerIds.contains(id)) { + parsedPeers.add(Peer.fromJson({'id': id})); + peerIds.add(id); + } + } + + peers = parsedPeers; + setState(() { + _isPeersLoading = false; + _isPeersLoaded = true; + }); + } +} + +class AutocompletePeerTile extends StatefulWidget { + final VoidCallback onSelect; + final Peer peer; + + const AutocompletePeerTile({ + Key? key, + required this.onSelect, + required this.peer, + }) : super(key: key); + + @override + AutocompletePeerTileState createState() => AutocompletePeerTileState(); +} + +class AutocompletePeerTileState extends State { + List _frontN(List list, int n) { + if (list.length <= n) { + return list; + } else { + return list.sublist(0, n); + } + } + + @override + Widget build(BuildContext context) { + final double tileRadius = 5; + final name = + '${widget.peer.username}${widget.peer.username.isNotEmpty && widget.peer.hostname.isNotEmpty ? '@' : ''}${widget.peer.hostname}'; + final greyStyle = TextStyle( + fontSize: 11, + color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); + final child = GestureDetector( + onTap: () => widget.onSelect(), + child: Padding( + padding: EdgeInsets.only(left: 5, right: 5), + child: Container( + height: 42, + margin: EdgeInsets.only(bottom: 5), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + decoration: BoxDecoration( + color: str2color( + '${widget.peer.id}${widget.peer.platform}', 0x7f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(tileRadius), + bottomLeft: Radius.circular(tileRadius), + ), + ), + alignment: Alignment.center, + width: 42, + height: null, + child: Padding( + padding: EdgeInsets.all(6), + child: getPlatformImage(widget.peer.platform, + size: 30))), + Expanded( + child: Container( + padding: EdgeInsets.only(left: 10), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.only( + topRight: Radius.circular(tileRadius), + bottomRight: Radius.circular(tileRadius), + ), + ), + child: Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 2), + child: Container( + margin: EdgeInsets.only(top: 2), + child: Column( + children: [ + Container( + margin: + EdgeInsets.only(top: 2), + child: Row(children: [ + getOnline( + 8, widget.peer.online), + Expanded( + child: Text( + widget.peer.alias.isEmpty + ? formatID( + widget.peer.id) + : widget.peer.alias, + overflow: + TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleSmall, + )), + widget.peer.alias.isNotEmpty + ? Padding( + padding: + const EdgeInsets + .only( + left: 5, + right: 5), + child: Text( + "(${widget.peer.id})", + style: greyStyle, + overflow: + TextOverflow + .ellipsis, + )) + : Container(), + ])), + Align( + alignment: Alignment.centerLeft, + child: Text( + name, + style: greyStyle, + textAlign: TextAlign.start, + overflow: + TextOverflow.ellipsis, + ), + ), + ], + )))), + ], + )), + ) + ], + )))); + final colors = _frontN(widget.peer.tags, 25) + .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) + .toList(); + return Tooltip( + message: !(isDesktop || isWebDesktop) + ? '' + : widget.peer.tags.isNotEmpty + ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' + : '', + child: Stack(children: [ + child, + if (colors.isNotEmpty) + Positioned( + top: 5, + right: 10, + child: CustomPaint( + painter: TagPainter(radius: 3, colors: colors), + ), + ) + ]), + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/chat_page.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/chat_page.dart new file mode 100644 index 0000000..4b0954d --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/chat_page.dart @@ -0,0 +1,180 @@ +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'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; + +import '../../mobile/pages/home_page.dart'; + +enum ChatPageType { + mobileMain, + desktopCM, +} + +class ChatPage extends StatelessWidget implements PageShape { + late final ChatModel chatModel; + final ChatPageType? type; + + ChatPage({ChatModel? chatModel, this.type}) { + this.chatModel = chatModel ?? gFFI.chatModel; + } + + @override + final title = translate("Chat"); + + @override + final icon = unreadTopRightBuilder(gFFI.chatModel.mobileUnreadSum); + + @override + final appBarActions = [ + PopupMenuButton( + tooltip: "", + icon: unreadTopRightBuilder(gFFI.chatModel.mobileUnreadSum, + 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 key = entry.key; + final user = entry.value.chatUser; + final client = gFFI.serverModel.clients + .firstWhereOrNull((e) => e.id == key.connId); + final connected = + gFFI.serverModel.clients.any((e) => e.id == key.connId); + return PopupMenuItem( + child: Row( + children: [ + Icon( + key.isOut + ? Icons.call_made_rounded + : Icons.call_received_rounded, + color: MyTheme.accent) + .marginOnly(right: 6), + Text("${user.firstName} ${user.id}"), + if (connected) + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color.fromARGB(255, 46, 205, 139)), + ).marginSymmetric(horizontal: 2), + if (client != null) + unreadMessageCountBuilder(client.unreadChatMessageCount) + .marginOnly(left: 4) + ], + ), + value: key, + ); + }).toList(); + }, + onSelected: (key) { + gFFI.chatModel.changeCurrentKey(key); + }) + ]; + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: chatModel, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Consumer( + builder: (context, chatModel, child) { + final readOnly = type == ChatPageType.mobileMain && + (chatModel.currentKey.connId == ChatModel.clientModeID || + gFFI.serverModel.clients.every((e) => + e.id != chatModel.currentKey.connId || + chatModel.currentUser == null)) || + type == ChatPageType.desktopCM && + gFFI.serverModel.clients + .firstWhereOrNull( + (e) => e.id == chatModel.currentKey.connId) + ?.disconnected == + true; + return Stack( + children: [ + LayoutBuilder(builder: (context, constraints) { + final chat = DashChat( + onSend: chatModel.send, + currentUser: chatModel.me, + messages: chatModel + .messages[chatModel.currentKey]?.chatMessages ?? + [], + readOnly: readOnly, + inputOptions: InputOptions( + focusNode: chatModel.inputNode, + textController: chatModel.textController, + inputTextStyle: TextStyle( + fontSize: 14, + color: Theme.of(context).textTheme.titleLarge?.color), + inputDecoration: InputDecoration( + isDense: true, + hintText: translate('Write a message'), + filled: true, + fillColor: Theme.of(context).colorScheme.background, + contentPadding: EdgeInsets.all(10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: const BorderSide( + width: 1, + style: BorderStyle.solid, + ), + ), + ), + sendButtonBuilder: defaultSendButton( + padding: + EdgeInsets.symmetric(horizontal: 6, vertical: 0), + color: MyTheme.accent, + icon: Icons.send_rounded, + ), + ), + messageOptions: MessageOptions( + showOtherUsersAvatar: false, + showOtherUsersName: false, + textColor: Colors.white, + maxWidth: constraints.maxWidth * 0.7, + messageTextBuilder: (message, _, __) { + final isOwnMessage = message.user.id.isBlank!; + return Column( + crossAxisAlignment: isOwnMessage + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Text(message.text, + style: TextStyle(color: Colors.white)), + Text( + "${message.createdAt.hour}:${message.createdAt.minute.toString().padLeft(2, '0')}", + style: TextStyle( + color: Colors.white, + fontSize: 8, + ), + ).marginOnly(top: 3), + ], + ); + }, + messageDecorationBuilder: + (message, previousMessage, nextMessage) { + final isOwnMessage = message.user.id.isBlank!; + return defaultMessageDecoration( + color: + isOwnMessage ? MyTheme.accent : Colors.blueGrey, + borderTopLeft: 8, + borderTopRight: 8, + borderBottomRight: isOwnMessage ? 2 : 8, + borderBottomLeft: isOwnMessage ? 8 : 2, + ); + }, + ), + ).workaroundFreezeLinuxMint(); + return SelectionArea(child: chat); + }), + ], + ).paddingOnly(bottom: 8); + }, + ), + ), + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/connection_page_title.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/connection_page_title.dart new file mode 100644 index 0000000..ba03c26 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/connection_page_title.dart @@ -0,0 +1,38 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +Widget getConnectionPageTitle(BuildContext context, bool isWeb) { + return Row( + children: [ + Expanded( + child: Row( + children: [ + AutoSizeText( + translate('Control Remote Desktop'), + maxLines: 1, + style: Theme.of(context) + .textTheme + .titleLarge + ?.merge(TextStyle(height: 1)), + ).marginOnly(right: 4), + Tooltip( + waitDuration: Duration(milliseconds: 300), + message: translate(isWeb ? "web_id_input_tip" : "id_input_tip"), + child: Icon( + Icons.help_outline_outlined, + size: 16, + color: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5), + ), + ), + ], + )), + ], + ); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/custom_password.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/custom_password.dart new file mode 100644 index 0000000..dafc23b --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/custom_password.dart @@ -0,0 +1,129 @@ +// https://github.com/rodrigobastosv/fancy_password_field +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:get/get.dart'; +import 'package:password_strength/password_strength.dart'; + +abstract class ValidationRule { + String get name; + bool validate(String value); +} + +class UppercaseValidationRule extends ValidationRule { + @override + String get name => translate('uppercase'); + @override + bool validate(String value) { + return value.runes.any((int rune) { + var character = String.fromCharCode(rune); + return character.toUpperCase() == character && + character.toLowerCase() != character; + }); + } +} + +class LowercaseValidationRule extends ValidationRule { + @override + String get name => translate('lowercase'); + + @override + bool validate(String value) { + return value.runes.any((int rune) { + var character = String.fromCharCode(rune); + return character.toLowerCase() == character && + character.toUpperCase() != character; + }); + } +} + +class DigitValidationRule extends ValidationRule { + @override + String get name => translate('digit'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[0-9]')); + } +} + +class SpecialCharacterValidationRule extends ValidationRule { + @override + String get name => translate('special character'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]')); + } +} + +class MinCharactersValidationRule extends ValidationRule { + final int _numberOfCharacters; + MinCharactersValidationRule(this._numberOfCharacters); + + @override + String get name => translate('length>=$_numberOfCharacters'); + + @override + bool validate(String value) { + return value.length >= _numberOfCharacters; + } +} + +class PasswordStrengthIndicator extends StatelessWidget { + final RxString password; + final double weakMedium = 0.33; + final double mediumStrong = 0.67; + const PasswordStrengthIndicator({Key? key, required this.password}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + var strength = estimatePasswordStrength(password.value); + return Row( + children: [ + Expanded( + child: _indicator( + password.isEmpty ? Colors.grey : _getColor(strength))), + Expanded( + child: _indicator(password.isEmpty || strength < weakMedium + ? Colors.grey + : _getColor(strength))), + Expanded( + child: _indicator(password.isEmpty || strength < mediumStrong + ? Colors.grey + : _getColor(strength))), + Text(password.isEmpty ? '' : translate(_getLabel(strength))) + .marginOnly(left: password.isEmpty ? 0 : 8), + ], + ); + }); + } + + Widget _indicator(Color color) { + return Container( + height: 8, + color: color, + ); + } + + String _getLabel(double strength) { + if (strength < weakMedium) { + return 'Weak'; + } else if (strength < mediumStrong) { + return 'Medium'; + } else { + return 'Strong'; + } + } + + Color _getColor(double strength) { + if (strength < weakMedium) { + return Colors.yellow; + } else if (strength < mediumStrong) { + return Colors.blue; + } else { + return Colors.green; + } + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/custom_scale_base.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/custom_scale_base.dart new file mode 100644 index 0000000..6eceef1 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/custom_scale_base.dart @@ -0,0 +1,156 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/utils/scale.dart'; +import 'package:flutter_hbb/common.dart'; + +/// Base class providing shared custom scale control logic for both mobile and desktop widgets. +/// Implementations must provide [ffi] and [onScaleChanged] getters. +abstract class CustomScaleControls extends State { + /// FFI instance for session interaction + FFI get ffi; + + /// Callback invoked when scale value changes + ValueChanged? get onScaleChanged; + + late int _scaleValue; + late final Debouncer _debouncerScale; + // Normalized slider position in [0, 1]. We map it nonlinearly to percent. + double _scalePos = 0.0; + + int get scaleValue => _scaleValue; + double get scalePos => _scalePos; + + int mapPosToPercent(double p) => _mapPosToPercent(p); + + static const int minPercent = kScaleCustomMinPercent; + static const int pivotPercent = kScaleCustomPivotPercent; // 100% should be at 1/3 of track + static const int maxPercent = kScaleCustomMaxPercent; + static const double pivotPos = kScaleCustomPivotPos; // first 1/3 → up to 100% + static const double detentEpsilon = kScaleCustomDetentEpsilon; // snap range around pivot (~0.6%) + + // Clamp helper for local use + int _clampScale(int v) => clampCustomScalePercent(v); + + // Map normalized position [0,1] → percent [5,1000] with 100 at 1/3 width. + int _mapPosToPercent(double p) { + if (p <= 0.0) return minPercent; + if (p >= 1.0) return maxPercent; + if (p <= pivotPos) { + final q = p / pivotPos; // 0..1 + final v = minPercent + q * (pivotPercent - minPercent); + return _clampScale(v.round()); + } else { + final q = (p - pivotPos) / (1.0 - pivotPos); // 0..1 + final v = pivotPercent + q * (maxPercent - pivotPercent); + return _clampScale(v.round()); + } + } + + // Map percent [5,1000] → normalized position [0,1] + double _mapPercentToPos(int percent) { + final p = _clampScale(percent); + if (p <= pivotPercent) { + final q = (p - minPercent) / (pivotPercent - minPercent); + return q * pivotPos; + } else { + final q = (p - pivotPercent) / (maxPercent - pivotPercent); + return pivotPos + q * (1.0 - pivotPos); + } + } + + // Snap normalized position to the pivot when close to it + double _snapNormalizedPos(double p) { + if ((p - pivotPos).abs() <= detentEpsilon) return pivotPos; + if (p < 0.0) return 0.0; + if (p > 1.0) return 1.0; + return p; + } + + @override + void initState() { + super.initState(); + _scaleValue = 100; + _debouncerScale = Debouncer( + kDebounceCustomScaleDuration, + onChanged: (v) async { + await _applyScale(v); + }, + initialValue: _scaleValue, + ); + WidgetsBinding.instance.addPostFrameCallback((_) async { + try { + final v = await getSessionCustomScalePercent(ffi.sessionId); + if (mounted) { + setState(() { + _scaleValue = v; + _scalePos = _mapPercentToPos(v); + }); + } + } catch (e, st) { + debugPrint('[CustomScale] Failed to get initial value: $e'); + debugPrintStack(stackTrace: st); + } + }); + } + + Future _applyScale(int v) async { + v = clampCustomScalePercent(v); + setState(() { + _scaleValue = v; + }); + try { + await bind.sessionSetFlutterOption( + sessionId: ffi.sessionId, + k: kCustomScalePercentKey, + v: v.toString()); + final curStyle = await bind.sessionGetViewStyle(sessionId: ffi.sessionId); + if (curStyle != kRemoteViewStyleCustom) { + await bind.sessionSetViewStyle( + sessionId: ffi.sessionId, value: kRemoteViewStyleCustom); + } + await ffi.canvasModel.updateViewStyle(); + if (isMobile) { + HapticFeedback.selectionClick(); + } + onScaleChanged?.call(v); + } catch (e, st) { + debugPrint('[CustomScale] Apply failed: $e'); + debugPrintStack(stackTrace: st); + } + } + + void nudgeScale(int delta) { + final next = _clampScale(_scaleValue + delta); + setState(() { + _scaleValue = next; + _scalePos = _mapPercentToPos(next); + }); + onScaleChanged?.call(next); + _debouncerScale.value = next; + } + + @override + void dispose() { + _debouncerScale.cancel(); + super.dispose(); + } + + void onSliderChanged(double v) { + final snapped = _snapNormalizedPos(v); + final next = _mapPosToPercent(snapped); + if (next != _scaleValue || snapped != _scalePos) { + setState(() { + _scalePos = snapped; + _scaleValue = next; + }); + onScaleChanged?.call(next); + _debouncerScale.value = next; + } + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/dialog.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/dialog.dart new file mode 100644 index 0000000..7534fb2 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/dialog.dart @@ -0,0 +1,2867 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:bot_toast/bot_toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:get/get.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:flutter_hbb/utils/http_service.dart' as http; + +import '../../common.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import 'address_book.dart'; + +void clientClose(SessionID sessionId, FFI ffi) async { + if (allowAskForNoteAtEndOfConnection(ffi, true)) { + if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) { + return; + } + closeConnection(); + } else { + msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?', + '', ffi.dialogManager); + } +} + +abstract class ValidationRule { + String get name; + bool validate(String value); +} + +class LengthRangeValidationRule extends ValidationRule { + final int _min; + final int _max; + + LengthRangeValidationRule(this._min, this._max); + + @override + String get name => translate('length %min% to %max%') + .replaceAll('%min%', _min.toString()) + .replaceAll('%max%', _max.toString()); + + @override + bool validate(String value) { + return value.length >= _min && value.length <= _max; + } +} + +class RegexValidationRule extends ValidationRule { + final String _name; + final RegExp _regex; + + RegexValidationRule(this._name, this._regex); + + @override + String get name => translate(_name); + + @override + bool validate(String value) { + return value.isNotEmpty ? value.contains(_regex) : false; + } +} + +void changeIdDialog() { + var newId = ""; + var msg = ""; + var isInProgress = false; + TextEditingController controller = TextEditingController(); + final RxString rxId = controller.text.trim().obs; + + final rules = [ + RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')), + LengthRangeValidationRule(6, 16), + RegexValidationRule('allowed characters', RegExp(r'^[\w-]*$')) + ]; + + gFFI.dialogManager.show((setState, close, context) { + submit() async { + debugPrint("onSubmit"); + newId = controller.text.trim(); + + final Iterable violations = rules.where((r) => !r.validate(newId)); + if (violations.isNotEmpty) { + setState(() { + msg = (isDesktop || isWebDesktop) + ? '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}' + : violations.map((r) => r.name).join(', '); + }); + return; + } + + setState(() { + msg = ""; + isInProgress = true; + bind.mainChangeId(newId: newId); + }); + + var status = await bind.mainGetAsyncStatus(); + while (status == " ") { + await Future.delayed(const Duration(milliseconds: 100)); + status = await bind.mainGetAsyncStatus(); + } + if (status.isEmpty) { + // ok + close(); + return; + } + setState(() { + isInProgress = false; + msg = (isDesktop || isWebDesktop) + ? '${translate('Prompt')}: ${translate(status)}' + : translate(status); + }); + } + + return CustomAlertDialog( + title: Text(translate("Change ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("id_change_tip")), + const SizedBox( + height: 12.0, + ), + TextField( + decoration: InputDecoration( + labelText: translate('Your new ID'), + errorText: msg.isEmpty ? null : translate(msg), + suffixText: '${rxId.value.length}/16', + suffixStyle: const TextStyle(fontSize: 12, color: Colors.grey)), + inputFormatters: [ + LengthLimitingTextInputFormatter(16), + // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) + ], + controller: controller, + autofocus: true, + onChanged: (value) { + setState(() { + rxId.value = value.trim(); + msg = ''; + }); + }, + ).workaroundFreezeLinuxMint(), + const SizedBox( + height: 8.0, + ), + (isDesktop || isWebDesktop) + ? Obx(() => Wrap( + runSpacing: 8, + spacing: 4, + children: rules.map((e) { + var checked = e.validate(rxId.value); + return Chip( + label: Text( + e.name, + style: TextStyle( + color: checked + ? const Color(0xFF0A9471) + : Color.fromARGB(255, 198, 86, 157)), + ), + backgroundColor: checked + ? const Color(0xFFD0F7ED) + : Color.fromARGB(255, 247, 205, 232)); + }).toList(), + )).marginOnly(bottom: 8) + : SizedBox.shrink(), + // NOT use Offstage to wrap LinearProgressIndicator + if (isInProgress) const LinearProgressIndicator(), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void changeWhiteList({Function()? callback}) async { + final curWhiteList = await bind.mainGetOption(key: kOptionWhitelist); + var newWhiteListField = curWhiteList == defaultOptionWhitelist + ? '' + : curWhiteList.split(',').join('\n'); + var controller = TextEditingController(text: newWhiteListField); + var msg = ""; + var isInProgress = false; + final isOptFixed = isOptionFixed(kOptionWhitelist); + gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + title: Text(translate("IP Whitelisting")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + decoration: InputDecoration( + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + enabled: !isOptFixed, + autofocus: true) + .workaroundFreezeLinuxMint(), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + // NOT use Offstage to wrap LinearProgressIndicator + if (isInProgress) const LinearProgressIndicator(), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + if (!isOptFixed) + dialogButton("Clear", onPressed: () async { + await bind.mainSetOption( + key: kOptionWhitelist, value: defaultOptionWhitelist); + callback?.call(); + close(); + }, isOutline: true), + if (!isOptFixed) + dialogButton( + "OK", + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + newWhiteListField = controller.text.trim(); + var newWhiteList = ""; + if (newWhiteListField.isEmpty) { + // pass + } else { + final ips = + newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); + // test ip + final ipMatch = RegExp( + r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$"); + final ipv6Match = RegExp( + r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$"); + for (final ip in ips) { + if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) { + msg = "${translate("Invalid IP")} $ip"; + setState(() { + isInProgress = false; + }); + return; + } + } + newWhiteList = ips.join(','); + } + if (newWhiteList.trim().isEmpty) { + newWhiteList = defaultOptionWhitelist; + } + await bind.mainSetOption( + key: kOptionWhitelist, value: newWhiteList); + callback?.call(); + close(); + }, + ), + ], + onCancel: close, + ); + }); +} + +Future changeDirectAccessPort( + String currentIP, String currentPort) async { + final controller = TextEditingController(text: currentPort); + await gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + title: Text(translate("Change Local Port")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8.0), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: '21118', + isCollapsed: true, + prefix: Text('$currentIP : '), + suffix: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.clear, size: 16), + onPressed: () => controller.clear())), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), + ], + controller: controller, + autofocus: true) + .workaroundFreezeLinuxMint(), + ), + ], + ), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: () async { + await bind.mainSetOption( + key: kOptionDirectAccessPort, value: controller.text); + close(); + }), + ], + onCancel: close, + ); + }); + return controller.text; +} + +Future changeAutoDisconnectTimeout(String old) async { + final controller = TextEditingController(text: old); + await gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + title: Text(translate("Timeout in minutes")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8.0), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: '10', + isCollapsed: true, + suffix: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.clear, size: 16), + onPressed: () => controller.clear())), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), + ], + controller: controller, + autofocus: true) + .workaroundFreezeLinuxMint(), + ), + ], + ), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: () async { + await bind.mainSetOption( + key: kOptionAutoDisconnectTimeout, value: controller.text); + close(); + }), + ], + onCancel: close, + ); + }); + return controller.text; +} + +class DialogTextField extends StatelessWidget { + final String title; + final String? hintText; + final bool obscureText; + final String? errorText; + final String? helperText; + final Widget? prefixIcon; + final Widget? suffixIcon; + final TextEditingController controller; + final FocusNode? focusNode; + final TextInputType? keyboardType; + final List? inputFormatters; + final int? maxLength; + + static const kUsernameTitle = 'Username'; + static const kUsernameIcon = Icon(Icons.account_circle_outlined); + static const kPasswordTitle = 'Password'; + static const kPasswordIcon = Icon(Icons.lock_outline); + + DialogTextField( + {Key? key, + this.focusNode, + this.obscureText = false, + this.errorText, + this.helperText, + this.prefixIcon, + this.suffixIcon, + this.hintText, + this.keyboardType, + this.inputFormatters, + this.maxLength, + required this.title, + required this.controller}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Column( + children: [ + TextField( + decoration: InputDecoration( + labelText: title, + hintText: hintText, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + helperText: helperText, + helperMaxLines: 8, + ), + controller: controller, + focusNode: focusNode, + autofocus: true, + obscureText: obscureText, + keyboardType: keyboardType, + inputFormatters: inputFormatters, + maxLength: maxLength, + ), + if (errorText != null) + Align( + alignment: Alignment.centerLeft, + child: SelectableText( + errorText!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + textAlign: TextAlign.left, + ).paddingOnly(top: 8, left: 12), + ), + ], + ).workaroundFreezeLinuxMint(), + ), + ], + ).paddingSymmetric(vertical: 4.0); + } +} + +abstract class ValidationField extends StatelessWidget { + ValidationField({Key? key}) : super(key: key); + + String? validate(); + bool get isReady; +} + +class Dialog2FaField extends ValidationField { + Dialog2FaField({ + Key? key, + required this.controller, + this.autoFocus = true, + this.reRequestFocus = false, + this.title, + this.hintText, + this.errorText, + this.readyCallback, + this.onChanged, + }) : super(key: key); + + final TextEditingController controller; + final bool autoFocus; + final bool reRequestFocus; + final String? title; + final String? hintText; + final String? errorText; + final VoidCallback? readyCallback; + final VoidCallback? onChanged; + final errMsg = translate('2FA code must be 6 digits.'); + + @override + Widget build(BuildContext context) { + return DialogVerificationCodeField( + title: title ?? translate('2FA code'), + controller: controller, + errorText: errorText, + autoFocus: autoFocus, + reRequestFocus: reRequestFocus, + hintText: hintText, + readyCallback: readyCallback, + onChanged: _onChanged, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9]')), + ], + ); + } + + String get text => controller.text; + bool get isAllDigits => text.codeUnits.every((e) => e >= 48 && e <= 57); + + @override + bool get isReady => text.length == 6 && isAllDigits; + + @override + String? validate() => isReady ? null : errMsg; + + _onChanged(StateSetter setState, SimpleWrapper errText) { + onChanged?.call(); + + if (text.length > 6) { + setState(() => errText.value = errMsg); + return; + } + + if (!isAllDigits) { + setState(() => errText.value = errMsg); + return; + } + + if (isReady) { + readyCallback?.call(); + return; + } + + if (errText.value != null) { + setState(() => errText.value = null); + } + } +} + +class DialogEmailCodeField extends ValidationField { + DialogEmailCodeField({ + Key? key, + required this.controller, + this.autoFocus = true, + this.reRequestFocus = false, + this.hintText, + this.errorText, + this.readyCallback, + this.onChanged, + }) : super(key: key); + + final TextEditingController controller; + final bool autoFocus; + final bool reRequestFocus; + final String? hintText; + final String? errorText; + final VoidCallback? readyCallback; + final VoidCallback? onChanged; + final errMsg = translate('Email verification code must be 6 characters.'); + + @override + Widget build(BuildContext context) { + return DialogVerificationCodeField( + title: translate('Verification code'), + controller: controller, + errorText: errorText, + autoFocus: autoFocus, + reRequestFocus: reRequestFocus, + hintText: hintText, + readyCallback: readyCallback, + helperText: translate('verification_tip'), + onChanged: _onChanged, + keyboardType: TextInputType.visiblePassword, + ); + } + + String get text => controller.text; + + @override + bool get isReady => text.length == 6; + + @override + String? validate() => isReady ? null : errMsg; + + _onChanged(StateSetter setState, SimpleWrapper errText) { + onChanged?.call(); + + if (text.length > 6) { + setState(() => errText.value = errMsg); + return; + } + + if (isReady) { + readyCallback?.call(); + return; + } + + if (errText.value != null) { + setState(() => errText.value = null); + } + } +} + +class DialogVerificationCodeField extends StatefulWidget { + DialogVerificationCodeField({ + Key? key, + required this.controller, + required this.title, + this.autoFocus = true, + this.reRequestFocus = false, + this.helperText, + this.hintText, + this.errorText, + this.textLength, + this.readyCallback, + this.onChanged, + this.keyboardType, + this.inputFormatters, + }) : super(key: key); + + final TextEditingController controller; + final bool autoFocus; + final bool reRequestFocus; + final String title; + final String? helperText; + final String? hintText; + final String? errorText; + final int? textLength; + final VoidCallback? readyCallback; + final Function(StateSetter setState, SimpleWrapper errText)? + onChanged; + final TextInputType? keyboardType; + final List? inputFormatters; + + @override + State createState() => + _DialogVerificationCodeField(); +} + +class _DialogVerificationCodeField extends State { + final _focusNode = FocusNode(); + Timer? _timer; + Timer? _timerReRequestFocus; + SimpleWrapper errorText = SimpleWrapper(null); + String _preText = ''; + + @override + void initState() { + super.initState(); + if (widget.autoFocus) { + _timer = + Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); + + if (widget.onChanged != null) { + widget.controller.addListener(() { + final text = widget.controller.text.trim(); + if (text == _preText) return; + widget.onChanged!(setState, errorText); + _preText = text; + }); + } + } + + // software secure keyboard will take the focus since flutter 3.13 + // request focus again when android account password obtain focus + if (isAndroid && widget.reRequestFocus) { + _focusNode.addListener(() { + if (_focusNode.hasFocus) { + _timerReRequestFocus?.cancel(); + _timerReRequestFocus = Timer( + Duration(milliseconds: 100), () => _focusNode.requestFocus()); + } + }); + } + } + + @override + void dispose() { + _timer?.cancel(); + _timerReRequestFocus?.cancel(); + _focusNode.unfocus(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DialogTextField( + title: widget.title, + controller: widget.controller, + errorText: widget.errorText ?? errorText.value, + focusNode: _focusNode, + helperText: widget.helperText, + keyboardType: widget.keyboardType, + inputFormatters: widget.inputFormatters, + ); + } +} + +class PasswordWidget extends StatefulWidget { + PasswordWidget({ + Key? key, + required this.controller, + this.autoFocus = true, + this.reRequestFocus = false, + this.hintText, + this.errorText, + this.title, + this.maxLength, + }) : super(key: key); + + final TextEditingController controller; + final bool autoFocus; + final bool reRequestFocus; + final String? hintText; + final String? errorText; + final String? title; + final int? maxLength; + + @override + State createState() => _PasswordWidgetState(); +} + +class _PasswordWidgetState extends State { + bool _passwordVisible = false; + final _focusNode = FocusNode(); + Timer? _timer; + Timer? _timerReRequestFocus; + + @override + void initState() { + super.initState(); + if (widget.autoFocus) { + _timer = + Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); + } + // software secure keyboard will take the focus since flutter 3.13 + // request focus again when android account password obtain focus + if (isAndroid && widget.reRequestFocus) { + _focusNode.addListener(() { + if (_focusNode.hasFocus) { + _timerReRequestFocus?.cancel(); + _timerReRequestFocus = Timer( + Duration(milliseconds: 100), () => _focusNode.requestFocus()); + } + }); + } + } + + @override + void dispose() { + _timer?.cancel(); + _timerReRequestFocus?.cancel(); + _focusNode.unfocus(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DialogTextField( + title: translate(widget.title ?? DialogTextField.kPasswordTitle), + hintText: translate(widget.hintText ?? 'Enter your password'), + controller: widget.controller, + prefixIcon: DialogTextField.kPasswordIcon, + suffixIcon: IconButton( + icon: Icon( + // Based on passwordVisible state choose the icon + _passwordVisible ? Icons.visibility : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), + onPressed: () { + // Update the state i.e. toggle the state of passwordVisible variable + setState(() { + _passwordVisible = !_passwordVisible; + }); + }, + ), + obscureText: !_passwordVisible, + errorText: widget.errorText, + focusNode: _focusNode, + maxLength: widget.maxLength, + ); + } +} + +void wrongPasswordDialog(SessionID sessionId, + OverlayDialogManager dialogManager, type, title, text) { + dialogManager.dismissAll(); + dialogManager.show((setState, close, context) { + cancel() { + close(); + closeConnection(); + } + + submit() { + enterPasswordDialog(sessionId, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + onSubmit: submit, + onCancel: cancel, + actions: [ + dialogButton( + 'Cancel', + onPressed: cancel, + isOutline: true, + ), + dialogButton( + 'Retry', + onPressed: submit, + ), + ]); + }); +} + +void enterPasswordDialog( + SessionID sessionId, OverlayDialogManager dialogManager) async { + await _connectDialog( + sessionId, + dialogManager, + passwordController: TextEditingController(), + ); +} + +void enterUserLoginDialog( + SessionID sessionId, + OverlayDialogManager dialogManager, + String osAccountDescTip, + bool canRememberAccount) async { + await _connectDialog( + sessionId, + dialogManager, + osUsernameController: TextEditingController(), + osPasswordController: TextEditingController(), + osAccountDescTip: osAccountDescTip, + canRememberAccount: canRememberAccount, + ); +} + +void enterUserLoginAndPasswordDialog( + SessionID sessionId, + OverlayDialogManager dialogManager, + String osAccountDescTip, + bool canRememberAccount) async { + await _connectDialog( + sessionId, + dialogManager, + osUsernameController: TextEditingController(), + osPasswordController: TextEditingController(), + passwordController: TextEditingController(), + osAccountDescTip: osAccountDescTip, + canRememberAccount: canRememberAccount, + ); +} + +_connectDialog( + SessionID sessionId, + OverlayDialogManager dialogManager, { + TextEditingController? osUsernameController, + TextEditingController? osPasswordController, + TextEditingController? passwordController, + String? osAccountDescTip, + bool canRememberAccount = true, +}) async { + final errUsername = ''.obs; + var rememberPassword = false; + if (passwordController != null) { + rememberPassword = + await bind.sessionGetRemember(sessionId: sessionId) ?? false; + } + var rememberAccount = false; + if (canRememberAccount && osUsernameController != null) { + rememberAccount = + await bind.sessionGetRemember(sessionId: sessionId) ?? false; + } + if (osUsernameController != null) { + osUsernameController.addListener(() { + if (errUsername.value.isNotEmpty) { + errUsername.value = ''; + } + }); + } + + dialogManager.dismissAll(); + dialogManager.show((setState, close, context) { + cancel() { + close(); + closeConnection(); + } + + submit() { + if (osUsernameController != null) { + if (osUsernameController.text.trim().isEmpty) { + errUsername.value = translate('Empty Username'); + setState(() {}); + return; + } + } + final osUsername = osUsernameController?.text.trim() ?? ''; + final osPassword = osPasswordController?.text.trim() ?? ''; + final password = passwordController?.text.trim() ?? ''; + if (passwordController != null && password.isEmpty) return; + if (rememberAccount) { + bind.sessionPeerOption( + sessionId: sessionId, name: 'os-username', value: osUsername); + bind.sessionPeerOption( + sessionId: sessionId, name: 'os-password', value: osPassword); + } + gFFI.login( + osUsername, + osPassword, + sessionId, + password, + rememberPassword, + ); + close(); + dialogManager.showLoading(translate('Logging in...'), + onCancel: closeConnection); + } + + descWidget(String text) { + return Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + text, + maxLines: 3, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 16), + ), + ), + Container( + height: 8, + ), + ], + ); + } + + rememberWidget( + String desc, + bool remember, + ValueChanged? onChanged, + ) { + return CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text(desc), + value: remember, + onChanged: onChanged, + ); + } + + osAccountWidget() { + if (osUsernameController == null || osPasswordController == null) { + return Offstage(); + } + return Column( + children: [ + if (osAccountDescTip != null) descWidget(translate(osAccountDescTip)), + DialogTextField( + title: translate(DialogTextField.kUsernameTitle), + controller: osUsernameController, + prefixIcon: DialogTextField.kUsernameIcon, + errorText: null, + ), + if (errUsername.value.isNotEmpty) + Align( + alignment: Alignment.centerLeft, + child: SelectableText( + errUsername.value, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + textAlign: TextAlign.left, + ).paddingOnly(left: 12, bottom: 2), + ), + PasswordWidget( + controller: osPasswordController, + autoFocus: false, + ), + if (canRememberAccount) + rememberWidget( + translate('remember_account_tip'), + rememberAccount, + (v) { + if (v != null) { + setState(() => rememberAccount = v); + } + }, + ), + ], + ); + } + + passwdWidget() { + if (passwordController == null) { + return Offstage(); + } + return Column( + children: [ + descWidget(translate('verify_rustdesk_password_tip')), + PasswordWidget( + controller: passwordController, + autoFocus: osUsernameController == null, + ), + rememberWidget( + translate('Remember password'), + rememberPassword, + (v) { + if (v != null) { + setState(() => rememberPassword = v); + } + }, + ), + ], + ); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('Password Required')).paddingOnly(left: 10), + ], + ), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + osAccountWidget(), + osUsernameController == null || passwordController == null + ? Offstage() + : Container(height: 12), + passwdWidget(), + ]), + actions: [ + dialogButton( + 'Cancel', + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + 'OK', + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }); +} + +void showWaitUacDialog( + SessionID sessionId, OverlayDialogManager dialogManager, String type) { + dialogManager.dismissAll(); + dialogManager.show( + tag: '$sessionId-wait-uac', + (setState, close, context) => CustomAlertDialog( + title: null, + content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'), + actions: [ + dialogButton( + 'OK', + icon: Icon(Icons.done_rounded), + onPressed: close, + ), + ], + )); +} + +// Another username && password dialog? +void showRequestElevationDialog( + SessionID sessionId, OverlayDialogManager dialogManager) { + RxString groupValue = ''.obs; + RxString errUser = ''.obs; + RxString errPwd = ''.obs; + TextEditingController userController = TextEditingController(); + TextEditingController pwdController = TextEditingController(); + + void onRadioChanged(String? value) { + if (value != null) { + groupValue.value = value; + } + } + + // TODO get from theme + final double fontSizeNote = 13.00; + + Widget OptionRequestPermissions = Obx( + () => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Radio( + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + value: '', + groupValue: groupValue.value, + onChanged: onRadioChanged, + ).marginOnly(right: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + hoverColor: Colors.transparent, + onTap: () => groupValue.value = '', + child: Text( + translate('Ask the remote user for authentication'), + ), + ).marginOnly(bottom: 10), + Text( + translate('Choose this if the remote account is administrator'), + style: TextStyle(fontSize: fontSizeNote), + ), + ], + ).marginOnly(top: 3), + ), + ], + ), + ); + + Widget OptionCredentials = Obx( + () => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Radio( + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + value: 'logon', + groupValue: groupValue.value, + onChanged: onRadioChanged, + ).marginOnly(right: 10), + Expanded( + child: InkWell( + hoverColor: Colors.transparent, + onTap: () => onRadioChanged('logon'), + child: Text( + translate('Transmit the username and password of administrator'), + ), + ).marginOnly(top: 4), + ), + ], + ), + ); + + Widget UacNote = Container( + padding: EdgeInsets.fromLTRB(10, 8, 8, 8), + decoration: BoxDecoration( + color: MyTheme.currentThemeMode() == ThemeMode.dark + ? Color.fromARGB(135, 87, 87, 90) + : Colors.grey[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey), + ), + child: Row( + children: [ + Icon(Icons.info_outline_rounded, size: 20).marginOnly(right: 10), + Expanded( + child: Text( + translate('still_click_uac_tip'), + style: TextStyle( + fontSize: fontSizeNote, fontWeight: FontWeight.normal), + ), + ) + ], + ), + ); + + var content = Obx( + () => Column( + children: [ + OptionRequestPermissions.marginOnly(bottom: 15), + OptionCredentials, + Offstage( + offstage: 'logon' != groupValue.value, + child: Column( + children: [ + UacNote.marginOnly(bottom: 10), + DialogTextField( + controller: userController, + title: translate('Username'), + hintText: translate('elevation_username_tip'), + prefixIcon: DialogTextField.kUsernameIcon, + errorText: errUser.isEmpty ? null : errUser.value, + ), + PasswordWidget( + controller: pwdController, + autoFocus: false, + errorText: errPwd.isEmpty ? null : errPwd.value, + ), + ], + ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0), + ).marginOnly(top: 10), + ], + ), + ); + + dialogManager.dismissAll(); + dialogManager.show(tag: '$sessionId-request-elevation', + (setState, close, context) { + void submit() { + if (groupValue.value == 'logon') { + if (userController.text.isEmpty) { + errUser.value = translate('Empty Username'); + return; + } + if (pwdController.text.isEmpty) { + errPwd.value = translate('Empty Password'); + return; + } + bind.sessionElevateWithLogon( + sessionId: sessionId, + username: userController.text, + password: pwdController.text); + } else { + bind.sessionElevateDirect(sessionId: sessionId); + } + close(); + showWaitUacDialog(sessionId, dialogManager, "wait-uac"); + } + + return CustomAlertDialog( + title: Text(translate('Request Elevation')), + content: content, + actions: [ + dialogButton( + 'Cancel', + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + 'OK', + icon: Icon(Icons.done_rounded), + onPressed: submit, + ) + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showOnBlockDialog( + SessionID sessionId, + String type, + String title, + String text, + OverlayDialogManager dialogManager, +) { + if (dialogManager.existing('$sessionId-wait-uac') || + dialogManager.existing('$sessionId-request-elevation')) { + return; + } + dialogManager.show(tag: '$sessionId-$type', (setState, close, context) { + void submit() { + close(); + showRequestElevationDialog(sessionId, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, + "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}"), + actions: [ + dialogButton('Wait', onPressed: close, isOutline: true), + dialogButton('Request Elevation', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showElevationError(SessionID sessionId, String type, String title, + String text, OverlayDialogManager dialogManager) { + dialogManager.show(tag: '$sessionId-$type', (setState, close, context) { + void submit() { + close(); + showRequestElevationDialog(sessionId, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + actions: [ + dialogButton('Cancel', onPressed: () { + close(); + }, isOutline: true), + if (text != 'No permission') dialogButton('Retry', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showWaitAcceptDialog(SessionID sessionId, String type, String title, + String text, OverlayDialogManager dialogManager) { + dialogManager.dismissAll(); + dialogManager.show((setState, close, context) { + onCancel() { + closeConnection(); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + actions: [ + dialogButton('Cancel', onPressed: onCancel, isOutline: true), + ], + onCancel: onCancel, + ); + }); +} + +void showRestartRemoteDevice(PeerInfo pi, String id, SessionID sessionId, + OverlayDialogManager dialogManager) async { + final res = await dialogManager + .show((setState, close, context) => CustomAlertDialog( + title: Row(children: [ + Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28), + Flexible( + child: Text(translate("Restart remote device")) + .paddingOnly(left: 10)), + ]), + content: Text( + "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: () => close(true), + ), + ], + onCancel: close, + onSubmit: () => close(true), + )); + if (res == true) bind.sessionRestartRemoteDevice(sessionId: sessionId); +} + +showSetOSPassword( + SessionID sessionId, + bool login, + OverlayDialogManager dialogManager, + String? osPassword, + Function()? closeCallback, +) async { + final controller = TextEditingController(); + osPassword ??= + await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ?? + ''; + var autoLogin = + await bind.sessionGetOption(sessionId: sessionId, arg: 'auto-login') != + ''; + controller.text = osPassword; + dialogManager.show((setState, close, context) { + closeWithCallback([dynamic]) { + close(); + if (closeCallback != null) closeCallback(); + } + + submit() { + var text = controller.text.trim(); + bind.sessionPeerOption( + sessionId: sessionId, name: 'os-password', value: text); + bind.sessionPeerOption( + sessionId: sessionId, + name: 'auto-login', + value: autoLogin ? 'Y' : ''); + if (text != '' && login) { + bind.sessionInputOsPassword(sessionId: sessionId, value: text); + } + closeWithCallback(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('OS Password')).paddingOnly(left: 10), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), + ), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: closeWithCallback, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: closeWithCallback, + ); + }); +} + +showSetOSAccount( + SessionID sessionId, + OverlayDialogManager dialogManager, +) async { + final usernameController = TextEditingController(); + final passwdController = TextEditingController(); + var username = + await bind.sessionGetOption(sessionId: sessionId, arg: 'os-username') ?? + ''; + var password = + await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ?? + ''; + usernameController.text = username; + passwdController.text = password; + dialogManager.show((setState, close, context) { + submit() { + final username = usernameController.text.trim(); + final password = usernameController.text.trim(); + bind.sessionPeerOption( + sessionId: sessionId, name: 'os-username', value: username); + bind.sessionPeerOption( + sessionId: sessionId, name: 'os-password', value: password); + close(); + } + + descWidget(String text) { + return Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + text, + maxLines: 3, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 16), + ), + ), + Container( + height: 8, + ), + ], + ); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('OS Account')).paddingOnly(left: 10), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + descWidget(translate("os_account_desk_tip")), + DialogTextField( + title: translate(DialogTextField.kUsernameTitle), + controller: usernameController, + prefixIcon: DialogTextField.kUsernameIcon, + errorText: null, + ), + PasswordWidget(controller: passwdController), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +Widget buildNoteTextField({ + required TextEditingController controller, + required VoidCallback onEscape, +}) { + final focusNode = FocusNode( + onKey: (FocusNode node, RawKeyEvent evt) { + if (evt.logicalKey.keyLabel == 'Enter') { + if (evt is RawKeyDownEvent) { + int pos = controller.selection.base.offset; + controller.text = + '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; + controller.selection = + TextSelection.fromPosition(TextPosition(offset: pos + 1)); + } + return KeyEventResult.handled; + } + if (evt.logicalKey.keyLabel == 'Esc') { + if (evt is RawKeyDownEvent) { + onEscape(); + } + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + ); + + return TextField( + autofocus: true, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + decoration: InputDecoration( + hintText: translate('input note here'), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: EdgeInsets.all(12), + ), + minLines: 5, + maxLines: null, + maxLength: 256, + controller: controller, + focusNode: focusNode, + ).workaroundFreezeLinuxMint(); +} + +showAuditDialog(FFI ffi) async { + final controller = TextEditingController( + text: bind.sessionGetLastAuditNote(sessionId: ffi.sessionId)); + ffi.dialogManager.show((setState, close, context) { + submit() { + var text = controller.text; + bind.sessionSendNote(sessionId: ffi.sessionId, note: text); + close(); + } + + return CustomAlertDialog( + title: Text(translate('Note')), + content: SizedBox( + width: 250, + height: 120, + child: buildNoteTextField( + controller: controller, + onEscape: close, + )), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit) + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +bool allowAskForNoteAtEndOfConnection(FFI? ffi, bool closedByControlling) { + if (ffi == null) { + return false; + } + return mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection) && + bind + .sessionGetAuditServerSync(sessionId: ffi.sessionId, typ: "conn") + .isNotEmpty && + bind.sessionGetAuditGuid(sessionId: ffi.sessionId).isNotEmpty && + bind.sessionGetLastAuditNote(sessionId: ffi.sessionId).isEmpty && + (!closedByControlling || + bind.willSessionCloseCloseSession(sessionId: ffi.sessionId)); +} + +// return value: close canceled +// true: return +// false: go on +Future desktopTryShowTabAuditDialogCloseCancelled( + {required String id, required DesktopTabController tabController}) async { + try { + final page = + tabController.state.value.tabs.firstWhere((tab) => tab.key == id).page; + final ffi = (page as dynamic).ffi; + final res = await showConnEndAuditDialogCloseCanceled(ffi: ffi); + return res; + } catch (e) { + debugPrint('Failed to show audit dialog: $e'); + return false; + } +} + +// return value: +// true: return +// false: go on +Future showConnEndAuditDialogCloseCanceled( + {required FFI ffi, String? type, String? title, String? text}) async { + final res = await _showConnEndAuditDialogCloseCanceled( + ffi: ffi, type: type, title: title, text: text); + if (res == true) { + return true; + } + return false; +} + +// return value: +// true: return +// false / null: go on +Future _showConnEndAuditDialogCloseCanceled({ + required FFI ffi, + String? type, + String? title, + String? text, +}) async { + final closedByControlling = type == null; + final showDialog = allowAskForNoteAtEndOfConnection(ffi, closedByControlling); + if (!showDialog) { + return false; + } + ffi.dialogManager.dismissAll(); + + Future updateAuditNoteByGuid(String auditGuid, String note) async { + debugPrint('Updating audit note for GUID: $auditGuid, note: $note'); + try { + final apiServer = await bind.mainGetApiServer(); + if (apiServer.isEmpty) { + debugPrint('API server is empty, cannot update audit note'); + return; + } + final url = '$apiServer/api/audit'; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode({ + 'guid': auditGuid, + 'note': note, + }); + + final response = await http.put( + Uri.parse(url), + headers: headers, + body: body, + ); + + if (response.statusCode == 200) { + debugPrint('Successfully updated audit note for GUID: $auditGuid'); + } else { + debugPrint( + 'Failed to update audit note. Status: ${response.statusCode}, Body: ${response.body}'); + } + } catch (e) { + debugPrint('Error updating audit note: $e'); + } + } + + final controller = TextEditingController(); + bool askForNote = + mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection); + final isOptFixed = isOptionFixed(kOptionAllowAskForNoteAtEndOfConnection); + bool isInProgress = false; + + return await ffi.dialogManager.show((setState, close, context) { + cancel() { + close(true); + } + + set() async { + if (isInProgress) return; + setState(() { + isInProgress = true; + }); + var text = controller.text; + if (text.isNotEmpty) { + await updateAuditNoteByGuid( + bind.sessionGetAuditGuid(sessionId: ffi.sessionId), text) + .timeout(const Duration(seconds: 6), onTimeout: () { + debugPrint('updateAuditNoteByGuid timeout after 6s'); + }); + } + // Save the "ask for note" preference + if (!isOptFixed) { + await mainSetLocalBoolOption( + kOptionAllowAskForNoteAtEndOfConnection, askForNote); + } + } + + submit() async { + await set(); + close(false); + } + + final buttons = [ + dialogButton('OK', onPressed: isInProgress ? null : submit) + ]; + if (type == 'relay-hint' || type == 'relay-hint2') { + buttons.add(dialogButton('Retry', onPressed: () async { + await set(); + close(true); + ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, false); + })); + if (type == 'relay-hint2') { + buttons.add(dialogButton('Connect via relay', onPressed: () async { + await set(); + close(true); + ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, true); + })); + } + } + if (closedByControlling) { + buttons.add(dialogButton('Cancel', + onPressed: isInProgress ? null : cancel, isOutline: true)); + } + + Widget content; + if (closedByControlling) { + content = SelectionArea( + child: msgboxContent( + 'info', 'Close', 'Are you sure to close the connection?')); + } else { + content = + SelectionArea(child: msgboxContent(type, title ?? '', text ?? '')); + } + + return CustomAlertDialog( + title: null, + content: SizedBox( + width: 350, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + content, + const SizedBox(height: 16), + SizedBox( + height: 120, + child: buildNoteTextField( + controller: controller, + onEscape: cancel, + ), + ), + if (!isOptFixed) ...[ + const SizedBox(height: 8), + InkWell( + onTap: () { + setState(() { + askForNote = !askForNote; + }); + }, + child: Row( + children: [ + Checkbox( + value: askForNote, + onChanged: (value) { + setState(() { + askForNote = value ?? false; + }); + }, + ), + Expanded( + child: Text( + translate('note-at-conn-end-tip'), + style: const TextStyle(fontSize: 13), + ), + ), + ], + ), + ), + ], + if (isInProgress) + const LinearProgressIndicator().marginOnly(top: 4), + ], + )), + actions: buttons, + onSubmit: submit, + onCancel: cancel, + ); + }); +} + +void showConfirmSwitchSidesDialog( + SessionID sessionId, String id, OverlayDialogManager dialogManager) async { + dialogManager.show((setState, close, context) { + submit() async { + await bind.sessionSwitchSides(sessionId: sessionId); + closeConnection(id: id); + } + + return CustomAlertDialog( + content: msgboxContent('info', 'Switch Sides', + 'Please confirm if you want to share your desktop?'), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async { + double initQuality = kDefaultQuality; + double initFps = kDefaultFps; + bool qualitySet = false; + bool fpsSet = false; + + bool? direct; + try { + direct = + ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect; + } catch (_) {} + bool hideFps = (await bind.mainIsUsingPublicServer() && direct != true) || + versionCmp(ffi.ffiModel.pi.version, '1.2.0') < 0; + bool hideMoreQuality = + (await bind.mainIsUsingPublicServer() && direct != true) || + versionCmp(ffi.ffiModel.pi.version, '1.2.2') < 0; + + setCustomValues({double? quality, double? fps}) async { + debugPrint("setCustomValues quality:$quality, fps:$fps"); + if (quality != null) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + sessionId: sessionId, value: quality.toInt()); + } + if (fps != null) { + fpsSet = true; + await bind.sessionSetCustomFps(sessionId: sessionId, fps: fps.toInt()); + } + if (!qualitySet) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + sessionId: sessionId, value: initQuality.toInt()); + } + if (!hideFps && !fpsSet) { + fpsSet = true; + await bind.sessionSetCustomFps( + sessionId: sessionId, fps: initFps.toInt()); + } + } + + final btnClose = dialogButton('Close', onPressed: () async { + await setCustomValues(); + ffi.dialogManager.dismissAll(); + }); + + // quality + final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId); + initQuality = quality != null && quality.isNotEmpty + ? quality[0].toDouble() + : kDefaultQuality; + if (initQuality < kMinQuality || + initQuality > (!hideMoreQuality ? kMaxMoreQuality : kMaxQuality)) { + initQuality = kDefaultQuality; + } + // fps + final fpsOption = + await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps'); + initFps = fpsOption == null + ? kDefaultFps + : double.tryParse(fpsOption) ?? kDefaultFps; + if (initFps < kMinFps || initFps > kMaxFps) { + initFps = kDefaultFps; + } + + final content = customImageQualityWidget( + initQuality: initQuality, + initFps: initFps, + setQuality: (v) => setCustomValues(quality: v), + setFps: (v) => setCustomValues(fps: v), + showFps: !hideFps, + showMoreQuality: !hideMoreQuality); + msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); +} + +trackpadSpeedDialog(SessionID sessionId, FFI ffi) async { + int initSpeed = ffi.inputModel.trackpadSpeed; + final curSpeed = SimpleWrapper(initSpeed); + final btnClose = dialogButton('Close', onPressed: () async { + if (curSpeed.value <= kMaxTrackpadSpeed && + curSpeed.value >= kMinTrackpadSpeed && + curSpeed.value != initSpeed) { + await bind.sessionSetTrackpadSpeed( + sessionId: sessionId, value: curSpeed.value); + await ffi.inputModel.updateTrackpadSpeed(); + } + ffi.dialogManager.dismissAll(); + }); + msgBoxCommon( + ffi.dialogManager, + 'Trackpad speed', + TrackpadSpeedWidget( + value: curSpeed, + ), + [btnClose]); +} + +void deleteConfirmDialog(Function onSubmit, String title) async { + gFFI.dialogManager.show( + (setState, close, context) { + submit() async { + await onSubmit(); + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.delete_rounded, + color: Colors.red, + ), + Expanded( + child: Text(title, overflow: TextOverflow.ellipsis).paddingOnly( + left: 10, + ), + ), + ], + ), + content: SizedBox.shrink(), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }, + ); +} + +void editAbTagDialog( + List currentTags, Function(List) onSubmit) { + var isInProgress = false; + + final tags = List.of(gFFI.abModel.currentAbTags); + var selectedTag = currentTags.obs; + + gFFI.dialogManager.show((setState, close, context) { + submit() async { + setState(() { + isInProgress = true; + }); + await onSubmit(selectedTag); + close(); + } + + return CustomAlertDialog( + title: Text(translate("Edit Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Wrap( + children: tags + .map((e) => AddressBookTag( + name: e, + tags: selectedTag, + onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + }, + showActionMenu: false)) + .toList(growable: false), + ), + ), + // NOT use Offstage to wrap LinearProgressIndicator + if (isInProgress) const LinearProgressIndicator(), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void editAbPeerNoteDialog(String id) { + var isInProgress = false; + final currentNote = gFFI.abModel.getPeerNote(id); + var controller = TextEditingController(text: currentNote); + + gFFI.dialogManager.show((setState, close, context) { + submit() async { + setState(() { + isInProgress = true; + }); + await gFFI.abModel.changeNote(id: id, note: controller.text); + close(); + } + + return CustomAlertDialog( + title: Text(translate("Edit note")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: controller, + autofocus: true, + maxLines: 3, + minLines: 1, + maxLength: 300, + decoration: InputDecoration( + labelText: translate('Note'), + ), + ).workaroundFreezeLinuxMint(), + // NOT use Offstage to wrap LinearProgressIndicator + if (isInProgress) const LinearProgressIndicator(), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void renameDialog( + {required String oldName, + FormFieldValidator? validator, + required ValueChanged onSubmit, + Function? onCancel}) async { + RxBool isInProgress = false.obs; + var controller = TextEditingController(text: oldName); + final formKey = GlobalKey(); + gFFI.dialogManager.show((setState, close, context) { + submit() async { + String text = controller.text.trim(); + if (validator != null && formKey.currentState?.validate() == false) { + return; + } + isInProgress.value = true; + onSubmit(text); + close(); + isInProgress.value = false; + } + + cancel() { + onCancel?.call(); + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.edit_rounded, color: MyTheme.accent), + Text(translate('Rename')).paddingOnly(left: 10), + ], + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + child: Form( + key: formKey, + child: TextFormField( + controller: controller, + autofocus: true, + decoration: InputDecoration(labelText: translate('Name')), + validator: validator, + ).workaroundFreezeLinuxMint(), + ), + ), + // NOT use Offstage to wrap LinearProgressIndicator + Obx(() => + isInProgress.value ? const LinearProgressIndicator() : Offstage()) + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }); +} + +void changeBot({Function()? callback}) async { + if (bind.mainHasValidBotSync()) { + await bind.mainSetOption(key: "bot", value: ""); + callback?.call(); + return; + } + String errorText = ''; + bool loading = false; + final controller = TextEditingController(); + gFFI.dialogManager.show((setState, close, context) { + onVerify() async { + final token = controller.text.trim(); + if (token == "") return; + loading = true; + errorText = ''; + setState(() {}); + final error = await bind.mainVerifyBot(token: token); + if (error == "") { + callback?.call(); + close(); + } else { + errorText = translate(error); + loading = false; + setState(() {}); + } + } + + final codeField = TextField( + autofocus: true, + controller: controller, + decoration: InputDecoration( + hintText: translate('Token'), + ), + ).workaroundFreezeLinuxMint(); + + return CustomAlertDialog( + title: Text(translate("Telegram bot")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(translate("enable-bot-desc"), + style: TextStyle(fontSize: 12)) + .marginOnly(bottom: 12), + Row(children: [Expanded(child: codeField)]), + if (errorText != '') + Text(errorText, style: TextStyle(color: Colors.red)) + .marginOnly(top: 12), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + loading + ? CircularProgressIndicator() + : dialogButton("OK", onPressed: onVerify), + ], + onCancel: close, + ); + }); +} + +void change2fa({Function()? callback}) async { + if (bind.mainHasValid2FaSync()) { + await bind.mainSetOption(key: "2fa", value: ""); + await bind.mainClearTrustedDevices(); + callback?.call(); + return; + } + var new2fa = (await bind.mainGenerate2Fa()); + final secretRegex = RegExp(r'secret=([^&]+)'); + final secret = secretRegex.firstMatch(new2fa)?.group(1); + String? errorText; + final controller = TextEditingController(); + gFFI.dialogManager.show((setState, close, context) { + onVerify() async { + if (await bind.mainVerify2Fa(code: controller.text.trim())) { + callback?.call(); + close(); + } else { + errorText = translate('wrong-2fa-code'); + } + } + + final codeField = Dialog2FaField( + controller: controller, + errorText: errorText, + onChanged: () => setState(() => errorText = null), + title: translate('Verification code'), + readyCallback: () { + onVerify(); + setState(() {}); + }, + ); + + getOnSubmit() => codeField.isReady ? onVerify : null; + + return CustomAlertDialog( + title: Text(translate("enable-2fa-title")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(translate("enable-2fa-desc"), + style: TextStyle(fontSize: 12)) + .marginOnly(bottom: 12), + SizedBox( + width: 160, + height: 160, + child: QrImageView( + backgroundColor: Colors.white, + data: new2fa, + version: QrVersions.auto, + size: 160, + gapless: false, + )).marginOnly(bottom: 6), + SelectableText(secret ?? '', style: TextStyle(fontSize: 12)) + .marginOnly(bottom: 12), + Row(children: [Expanded(child: codeField)]), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: getOnSubmit()), + ], + onCancel: close, + ); + }); +} + +void enter2FaDialog( + SessionID sessionId, OverlayDialogManager dialogManager) async { + final controller = TextEditingController(); + final RxBool submitReady = false.obs; + final RxBool trustThisDevice = false.obs; + + dialogManager.dismissAll(); + dialogManager.show((setState, close, context) { + cancel() { + close(); + closeConnection(); + } + + submit() { + gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value); + close(); + dialogManager.showLoading(translate('Logging in...'), + onCancel: closeConnection); + } + + late Dialog2FaField codeField; + + codeField = Dialog2FaField( + controller: controller, + title: translate('Verification code'), + onChanged: () => submitReady.value = codeField.isReady, + ); + + final trustField = Obx(() => CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text(translate("Trust this device")), + value: trustThisDevice.value, + onChanged: (value) { + if (value == null) return; + trustThisDevice.value = value; + }, + )); + + return CustomAlertDialog( + title: Text(translate('enter-2fa-title')), + content: Column( + children: [ + codeField, + if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId)) + trustField, + ], + ), + actions: [ + dialogButton('Cancel', + onPressed: cancel, + isOutline: true, + style: TextStyle( + color: Theme.of(context).textTheme.bodyMedium?.color)), + Obx(() => dialogButton( + 'OK', + onPressed: submitReady.isTrue ? submit : null, + )), + ], + onSubmit: submit, + onCancel: cancel); + }); +} + +// This dialog should not be dismissed, otherwise it will be black screen, have not reproduced this. +void showWindowsSessionsDialog( + String type, + String title, + String text, + OverlayDialogManager dialogManager, + SessionID sessionId, + String peerId, + String sessions) { + List sessionsList = []; + try { + sessionsList = json.decode(sessions); + } catch (e) { + print(e); + } + List sids = []; + List names = []; + for (var session in sessionsList) { + sids.add(session['sid']); + names.add(session['name']); + } + String selectedUserValue = sids.first; + dialogManager.dismissAll(); + dialogManager.show((setState, close, context) { + submit() { + bind.sessionSendSelectedSessionId( + sessionId: sessionId, sid: selectedUserValue); + close(); + } + + return CustomAlertDialog( + title: null, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + msgboxContent(type, title, text).marginOnly(bottom: 12), + ComboBox( + keys: sids, + values: names, + initialKey: selectedUserValue, + onChanged: (value) { + selectedUserValue = value; + }), + ], + ), + actions: [ + dialogButton('Connect', onPressed: submit, isOutline: false), + ], + ); + }); +} + +void addPeersToAbDialog( + List peers, +) async { + Future addTo(String abname) async { + final mapList = peers.map((e) { + var json = e.toJson(); + // remove password when add to another address book to avoid re-share + json.remove('password'); + json.remove('hash'); + return json; + }).toList(); + final errMsg = await gFFI.abModel.addPeersTo(mapList, abname); + if (errMsg == null) { + showToast(translate('Successful')); + return true; + } else { + BotToast.showText(text: errMsg, contentColor: Colors.red); + return false; + } + } + + // if only one address book and it is personal, add to it directly + if (gFFI.abModel.addressbooks.length == 1 && + gFFI.abModel.current.isPersonal()) { + await addTo(gFFI.abModel.currentName.value); + return; + } + + RxBool isInProgress = false.obs; + final names = gFFI.abModel.addressBooksCanWrite(); + RxString currentName = gFFI.abModel.currentName.value.obs; + TextEditingController controller = TextEditingController(); + if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) { + names.remove(currentName.value); + } + if (names.isEmpty) { + debugPrint('no address book to add peers to, should not happen'); + return; + } + if (!names.contains(currentName.value)) { + currentName.value = names[0]; + } + gFFI.dialogManager.show((setState, close, context) { + submit() async { + if (controller.text != gFFI.abModel.translatedName(currentName.value)) { + BotToast.showText( + text: 'illegal address book name: ${controller.text}', + contentColor: Colors.red); + return; + } + isInProgress.value = true; + if (await addTo(currentName.value)) { + close(); + } + isInProgress.value = false; + } + + cancel() { + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(IconFont.addressBook, color: MyTheme.accent), + Text(translate('Add to address book')).paddingOnly(left: 10), + ], + ), + content: Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // https://github.com/flutter/flutter/issues/145081 + DropdownMenu( + initialSelection: currentName.value, + onSelected: (value) { + if (value != null) { + currentName.value = value; + } + }, + dropdownMenuEntries: names + .map((e) => DropdownMenuEntry( + value: e, label: gFFI.abModel.translatedName(e))) + .toList(), + inputDecorationTheme: InputDecorationTheme( + isDense: true, border: UnderlineInputBorder()), + enableFilter: true, + controller: controller, + ), + // NOT use Offstage to wrap LinearProgressIndicator + isInProgress.value ? const LinearProgressIndicator() : Offstage() + ], + )), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }); +} + +void setSharedAbPasswordDialog(String abName, Peer peer) { + TextEditingController controller = TextEditingController(text: ''); + RxBool isInProgress = false.obs; + RxBool isInputEmpty = true.obs; + bool passwordVisible = false; + controller.addListener(() { + isInputEmpty.value = controller.text.isEmpty; + }); + gFFI.dialogManager.show((setState, close, context) { + change(String password) async { + isInProgress.value = true; + bool res = + await gFFI.abModel.changeSharedPassword(abName, peer.id, password); + isInProgress.value = false; + if (res) { + showToast(translate('Successful')); + } + close(); + } + + cancel() { + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.key, color: MyTheme.accent), + Text(translate(peer.password.isEmpty + ? 'Set shared password' + : 'Change Password')) + .paddingOnly(left: 10), + ], + ), + content: Obx(() => Column(children: [ + TextField( + controller: controller, + autofocus: true, + obscureText: !passwordVisible, + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon( + passwordVisible ? Icons.visibility : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), + onPressed: () { + setState(() { + passwordVisible = !passwordVisible; + }); + }, + ), + ), + ).workaroundFreezeLinuxMint(), + if (!gFFI.abModel.current.isPersonal()) + Row(children: [ + Icon(Icons.info, color: Colors.amber).marginOnly(right: 4), + Text( + translate('share_warning_tip'), + style: TextStyle(fontSize: 12), + ) + ]).marginSymmetric(vertical: 10), + // NOT use Offstage to wrap LinearProgressIndicator + isInProgress.value ? const LinearProgressIndicator() : Offstage() + ])), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + if (peer.password.isNotEmpty) + dialogButton( + "Remove", + icon: Icon(Icons.delete_outline_rounded), + onPressed: () => change(''), + buttonStyle: ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.red)), + ), + Obx(() => dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: + isInputEmpty.value ? null : () => change(controller.text), + )), + ], + onSubmit: isInputEmpty.value ? null : () => change(controller.text), + onCancel: cancel, + ); + }); +} + +void CommonConfirmDialog(OverlayDialogManager dialogManager, String content, + VoidCallback onConfirm) { + dialogManager.show((setState, close, context) { + submit() { + close(); + onConfirm.call(); + } + + return CustomAlertDialog( + content: Row( + children: [ + Expanded( + child: Text(content, + style: const TextStyle(fontSize: 15), + textAlign: TextAlign.start), + ), + ], + ).marginOnly(bottom: 12), + actions: [ + dialogButton(translate("Cancel"), onPressed: close, isOutline: true), + dialogButton(translate("OK"), onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void changeUnlockPinDialog(String oldPin, Function() callback) { + final pinController = TextEditingController(text: oldPin); + final confirmController = TextEditingController(text: oldPin); + String? pinErrorText; + String? confirmationErrorText; + final maxLength = bind.mainMaxEncryptLen(); + gFFI.dialogManager.show((setState, close, context) { + submit() async { + pinErrorText = null; + confirmationErrorText = null; + final pin = pinController.text.trim(); + final confirm = confirmController.text.trim(); + if (pin != confirm) { + setState(() { + confirmationErrorText = + translate('The confirmation is not identical.'); + }); + return; + } + final errorMsg = bind.mainSetUnlockPin(pin: pin); + if (errorMsg != '') { + setState(() { + pinErrorText = translate(errorMsg); + }); + return; + } + callback.call(); + close(); + } + + return CustomAlertDialog( + title: Text(translate("Set PIN")), + content: Column( + children: [ + DialogTextField( + title: 'PIN', + controller: pinController, + obscureText: true, + errorText: pinErrorText, + maxLength: maxLength, + ), + DialogTextField( + title: translate('Confirmation'), + controller: confirmController, + obscureText: true, + errorText: confirmationErrorText, + maxLength: maxLength, + ) + ], + ).marginOnly(bottom: 12), + actions: [ + dialogButton(translate("Cancel"), onPressed: close, isOutline: true), + dialogButton(translate("OK"), onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void checkUnlockPinDialog(String correctPin, Function() passCallback) { + final controller = TextEditingController(); + String? errorText; + gFFI.dialogManager.show((setState, close, context) { + submit() async { + final pin = controller.text.trim(); + if (correctPin != pin) { + setState(() { + errorText = translate('Wrong PIN'); + }); + return; + } + passCallback.call(); + close(); + } + + return CustomAlertDialog( + content: Row( + children: [ + Expanded( + child: PasswordWidget( + title: 'PIN', + controller: controller, + errorText: errorText, + hintText: '', + )) + ], + ).marginOnly(bottom: 12), + actions: [ + dialogButton(translate("Cancel"), onPressed: close, isOutline: true), + dialogButton(translate("OK"), onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void confrimDeleteTrustedDevicesDialog( + RxList trustedDevices, RxList selectedDevices) { + CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?', + () async { + if (selectedDevices.isEmpty) return; + if (selectedDevices.length == trustedDevices.length) { + await bind.mainClearTrustedDevices(); + trustedDevices.clear(); + selectedDevices.clear(); + } else { + final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList()); + await bind.mainRemoveTrustedDevices(json: json); + trustedDevices.removeWhere((element) { + return selectedDevices.contains(element.hwid); + }); + selectedDevices.clear(); + } + }); +} + +void manageTrustedDeviceDialog() async { + RxList trustedDevices = (await TrustedDevice.get()).obs; + RxList selectedDevices = RxList.empty(); + gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + title: Text(translate("Manage trusted devices")), + content: trustedDevicesTable(trustedDevices, selectedDevices), + actions: [ + Obx(() => dialogButton(translate("Delete"), + onPressed: selectedDevices.isEmpty + ? null + : () { + confrimDeleteTrustedDevicesDialog( + trustedDevices, + selectedDevices, + ); + }, + isOutline: false) + .marginOnly(top: 12)), + dialogButton(translate("Close"), onPressed: close, isOutline: true) + .marginOnly(top: 12), + ], + onCancel: close, + ); + }); +} + +class TrustedDevice { + late final Uint8List hwid; + late final int time; + late final String id; + late final String name; + late final String platform; + + TrustedDevice.fromJson(Map json) { + final hwidList = json['hwid'] as List; + hwid = Uint8List.fromList(hwidList.cast()); + time = json['time']; + id = json['id']; + name = json['name']; + platform = json['platform']; + } + + String daysRemaining() { + final expiry = time + 90 * 24 * 60 * 60 * 1000; + final remaining = expiry - DateTime.now().millisecondsSinceEpoch; + if (remaining < 0) { + return '0'; + } + return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0); + } + + static Future> get() async { + final List devices = List.empty(growable: true); + try { + final devicesJson = await bind.mainGetTrustedDevices(); + if (devicesJson.isNotEmpty) { + final devicesList = json.decode(devicesJson); + if (devicesList is List) { + for (var device in devicesList) { + devices.add(TrustedDevice.fromJson(device)); + } + } + } + } catch (e) { + print(e.toString()); + } + devices.sort((a, b) => b.time.compareTo(a.time)); + return devices; + } +} + +Widget trustedDevicesTable( + RxList devices, RxList selectedDevices) { + RxBool selectAll = false.obs; + setSelectAll() { + if (selectedDevices.isNotEmpty && + selectedDevices.length == devices.length) { + selectAll.value = true; + } else { + selectAll.value = false; + } + } + + devices.listen((_) { + setSelectAll(); + }); + selectedDevices.listen((_) { + setSelectAll(); + }); + return FittedBox( + child: Obx(() => DataTable( + columns: [ + DataColumn( + label: Checkbox( + value: selectAll.value, + onChanged: (value) { + if (value == true) { + selectedDevices.clear(); + selectedDevices.addAll(devices.map((e) => e.hwid)); + } else { + selectedDevices.clear(); + } + }, + )), + DataColumn(label: Text(translate('Platform'))), + DataColumn(label: Text(translate('ID'))), + DataColumn(label: Text(translate('Username'))), + DataColumn(label: Text(translate('Days remaining'))), + ], + rows: devices.map((device) { + return DataRow(cells: [ + DataCell(Checkbox( + value: selectedDevices.contains(device.hwid), + onChanged: (value) { + if (value == null) return; + if (value) { + selectedDevices.remove(device.hwid); + selectedDevices.add(device.hwid); + } else { + selectedDevices.remove(device.hwid); + } + }, + )), + DataCell(Text(device.platform)), + DataCell(Text(device.id)), + DataCell(Text(device.name)), + DataCell(Text(device.daysRemaining())), + ]); + }).toList(), + )), + ); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/gestures.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/gestures.dart new file mode 100644 index 0000000..0501ca4 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/gestures.dart @@ -0,0 +1,797 @@ +import 'dart:async'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hbb/common/widgets/remote_input.dart'; + +enum GestureState { + none, + oneFingerPan, + twoFingerScale, + threeFingerVerticalDrag +} + +class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { + CustomTouchGestureRecognizer({ + Object? debugOwner, + Set? supportedDevices, + }) : super( + debugOwner: debugOwner, + supportedDevices: supportedDevices, + ) { + _init(); + } + + // oneFingerPan + GestureDragStartCallback? onOneFingerPanStart; + GestureDragUpdateCallback? onOneFingerPanUpdate; + GestureDragEndCallback? onOneFingerPanEnd; + GestureDragCancelCallback? onOneFingerPanCancel; + + // twoFingerScale : scale + pan event + GestureScaleStartCallback? onTwoFingerScaleStart; + GestureScaleUpdateCallback? onTwoFingerScaleUpdate; + GestureScaleEndCallback? onTwoFingerScaleEnd; + + // threeFingerVerticalDrag + GestureDragStartCallback? onThreeFingerVerticalDragStart; + GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate; + GestureDragEndCallback? onThreeFingerVerticalDragEnd; + + var _currentState = GestureState.none; + Timer? _debounceTimer; + + void _init() { + debugPrint("CustomTouchGestureRecognizer init"); + // onStart = (d) {}; + onUpdate = (d) { + _debounceTimer?.cancel(); + if (d.pointerCount == 1 && _currentState != GestureState.oneFingerPan) { + onOneFingerStartDebounce(d); + } else if (d.pointerCount == 2 && + _currentState != GestureState.twoFingerScale) { + onTwoFingerStartDebounce(d); + } else if (d.pointerCount == 3 && + _currentState != GestureState.threeFingerVerticalDrag) { + _currentState = GestureState.threeFingerVerticalDrag; + if (onThreeFingerVerticalDragStart != null) { + onThreeFingerVerticalDragStart!( + DragStartDetails(globalPosition: d.localFocalPoint)); + } + debugPrint("start threeFingerScale"); + } + if (_currentState != GestureState.none) { + switch (_currentState) { + case GestureState.oneFingerPan: + if (onOneFingerPanUpdate != null) { + onOneFingerPanUpdate!(_getDragUpdateDetails(d)); + } + break; + case GestureState.twoFingerScale: + if (onTwoFingerScaleUpdate != null) { + onTwoFingerScaleUpdate!(d); + } + break; + case GestureState.threeFingerVerticalDrag: + if (onThreeFingerVerticalDragUpdate != null) { + onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d)); + } + break; + default: + break; + } + return; + } + }; + onEnd = (d) { + debugPrint("ScaleGestureRecognizer onEnd"); + _debounceTimer?.cancel(); + // end + switch (_currentState) { + case GestureState.oneFingerPan: + debugPrint("OneFingerState.pan onEnd"); + if (onOneFingerPanEnd != null) { + onOneFingerPanEnd!(_getDragEndDetails(d)); + } + break; + case GestureState.twoFingerScale: + debugPrint("TwoFingerState.scale onEnd"); + if (onTwoFingerScaleEnd != null) { + onTwoFingerScaleEnd!(d); + } + if (isSpecialHoldDragActive) { + // If we are in special drag mode, we need to reset the state. + // Otherwise, the next `onTwoFingerScaleUpdate()` will handle a wrong `focalPoint`. + _currentState = GestureState.none; + return; + } + break; + case GestureState.threeFingerVerticalDrag: + debugPrint("ThreeFingerState.vertical onEnd"); + if (onThreeFingerVerticalDragEnd != null) { + onThreeFingerVerticalDragEnd!(_getDragEndDetails(d)); + } + break; + default: + break; + } + _debounceTimer = Timer(Duration(milliseconds: 200), () { + _currentState = GestureState.none; + }); + }; + } + + // FIXME: This debounce logic is not working properly. + // If we move our finger very fast, we won't be able to detect the "oneFingerPan" event sometimes. + void onOneFingerStartDebounce(ScaleUpdateDetails d) { + start(ScaleUpdateDetails d) { + _currentState = GestureState.oneFingerPan; + if (onOneFingerPanStart != null) { + onOneFingerPanStart!(DragStartDetails( + localPosition: d.localFocalPoint, globalPosition: d.focalPoint)); + } + } + + if (_currentState != GestureState.none) { + _debounceTimer = Timer(Duration(milliseconds: 200), () { + start(d); + debugPrint("debounce start oneFingerPan"); + }); + } else { + start(d); + debugPrint("start oneFingerPan"); + } + } + + void onTwoFingerStartDebounce(ScaleUpdateDetails d) { + start(ScaleUpdateDetails d) { + _currentState = GestureState.twoFingerScale; + if (onTwoFingerScaleStart != null) { + onTwoFingerScaleStart!(ScaleStartDetails( + localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint)); + } + } + + if (_currentState == GestureState.threeFingerVerticalDrag) { + _debounceTimer = Timer(Duration(milliseconds: 200), () { + start(d); + debugPrint("debounce start twoFingerScale"); + }); + } else { + start(d); + debugPrint("start twoFingerScale"); + } + } + + DragUpdateDetails _getDragUpdateDetails(ScaleUpdateDetails d) => + DragUpdateDetails( + globalPosition: d.focalPoint, + localPosition: d.localFocalPoint, + delta: d.focalPointDelta); + + DragEndDetails _getDragEndDetails(ScaleEndDetails d) => + DragEndDetails(velocity: d.velocity); + + @override + void rejectGesture(int pointer) { + super.rejectGesture(pointer); + switch (_currentState) { + case GestureState.oneFingerPan: + if (onOneFingerPanCancel != null) { + onOneFingerPanCancel!(); + } + break; + case GestureState.twoFingerScale: + // Reset scale state if needed, currently self-contained + break; + case GestureState.threeFingerVerticalDrag: + // Reset drag state if needed, currently self-contained + break; + default: + break; + } + _currentState = GestureState.none; + } +} + +class HoldTapMoveGestureRecognizer extends GestureRecognizer { + HoldTapMoveGestureRecognizer({ + Object? debugOwner, + Set? supportedDevices, + }) : super( + debugOwner: debugOwner, + supportedDevices: supportedDevices, + ); + + GestureDragStartCallback? onHoldDragStart; + GestureDragUpdateCallback? onHoldDragUpdate; + GestureDragDownCallback? onHoldDragDown; + GestureDragCancelCallback? onHoldDragCancel; + GestureDragEndCallback? onHoldDragEnd; + + bool _isStart = false; + + Timer? _firstTapUpTimer; + Timer? _secondTapDownTimer; + _TapTracker? _firstTap; + _TapTracker? _secondTap; + + PointerDownEvent? _lastPointerDownEvent; + + final Map _trackers = {}; + + @override + bool isPointerAllowed(PointerDownEvent event) { + if (_firstTap == null) { + switch (event.buttons) { + case kPrimaryButton: + if (onHoldDragStart == null && + onHoldDragUpdate == null && + onHoldDragCancel == null && + onHoldDragEnd == null) { + return false; + } + break; + default: + return false; + } + } + return super.isPointerAllowed(event); + } + + @override + void addAllowedPointer(PointerDownEvent event) { + if (_firstTap != null) { + if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) { + // Ignore out-of-bounds second taps. + return; + } else if (!_firstTap!.hasElapsedMinTime() || + !_firstTap!.hasSameButton(event)) { + // Restart when the second tap is too close to the first (touch screens + // often detect touches intermittently), or when buttons mismatch. + _reset(); + return _trackTap(event); + } else if (onHoldDragDown != null) { + invokeCallback( + 'onHoldDragDown', + () => onHoldDragDown!(DragDownDetails( + globalPosition: event.position, + localPosition: event.localPosition))); + } + } + _trackTap(event); + } + + void _trackTap(PointerDownEvent event) { + _stopFirstTapUpTimer(); + _stopSecondTapDownTimer(); + final _TapTracker tracker = _TapTracker( + event: event, + entry: GestureBinding.instance.gestureArena.add(event.pointer, this), + doubleTapMinTime: kDoubleTapMinTime, + gestureSettings: gestureSettings, + ); + _trackers[event.pointer] = tracker; + _lastPointerDownEvent = event; + tracker.startTrackingPointer(_handleEvent, event.transform); + } + + void _handleEvent(PointerEvent event) { + final _TapTracker tracker = _trackers[event.pointer]!; + if (event is PointerUpEvent) { + if (_firstTap == null && _secondTap == null) { + _registerFirstTap(tracker); + } else if (_secondTap != null) { + if (event.pointer == _secondTap!.pointer) { + if (onHoldDragEnd != null) { + onHoldDragEnd!(DragEndDetails()); + _secondTap = null; + _isStart = false; + } + } + } else { + _reject(tracker); + } + } else if (event is PointerDownEvent) { + if (_firstTap != null && _secondTap == null) { + _registerSecondTap(tracker); + } + } else if (event is PointerMoveEvent) { + if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) { + if (_firstTap != null && _firstTap!.pointer == event.pointer) { + // first tap move + _reject(tracker); + } else if (_secondTap != null && _secondTap!.pointer == event.pointer) { + // debugPrint("_secondTap move"); + // second tap move + if (!_isStart) { + _resolve(); + } + if (onHoldDragUpdate != null) { + onHoldDragUpdate!(DragUpdateDetails( + globalPosition: event.position, + localPosition: event.localPosition, + delta: event.delta)); + } + } + } + } else if (event is PointerCancelEvent) { + _reject(tracker); + } + } + + @override + void acceptGesture(int pointer) {} + + @override + void rejectGesture(int pointer) { + _TapTracker? tracker = _trackers[pointer]; + // If tracker isn't in the list, check if this is the first tap tracker + if (tracker == null && _firstTap != null && _firstTap!.pointer == pointer) { + tracker = _firstTap; + } + // If tracker is still null, we rejected ourselves already + if (tracker != null) { + _reject(tracker); + } + } + + void _resolve() { + _stopSecondTapDownTimer(); + _firstTap?.entry.resolve(GestureDisposition.accepted); + _secondTap?.entry.resolve(GestureDisposition.accepted); + _isStart = true; + // TODO start details + if (onHoldDragStart != null) { + onHoldDragStart!(DragStartDetails( + kind: _lastPointerDownEvent?.kind, + )); + } + } + + void _reject(_TapTracker tracker) { + try { + _checkCancel(); + _isStart = false; + _trackers.remove(tracker.pointer); + tracker.entry.resolve(GestureDisposition.rejected); + _freezeTracker(tracker); + _reset(); + } catch (e) { + debugPrint("Failed to _reject:$e"); + } + } + + @override + void dispose() { + _reset(); + super.dispose(); + } + + void _reset() { + _isStart = false; + // debugPrint("reset"); + _stopFirstTapUpTimer(); + _stopSecondTapDownTimer(); + if (_firstTap != null) { + if (_trackers.isNotEmpty) { + _checkCancel(); + } + // Note, order is important below in order for the resolve -> reject logic + // to work properly. + final _TapTracker tracker = _firstTap!; + _firstTap = null; + _reject(tracker); + GestureBinding.instance.gestureArena.release(tracker.pointer); + + if (_secondTap != null) { + final _TapTracker tracker = _secondTap!; + _secondTap = null; + _reject(tracker); + GestureBinding.instance.gestureArena.release(tracker.pointer); + } + } + _firstTap = null; + _secondTap = null; + _clearTrackers(); + } + + void _registerFirstTap(_TapTracker tracker) { + _startFirstTapUpTimer(); + GestureBinding.instance.gestureArena.hold(tracker.pointer); + // Note, order is important below in order for the clear -> reject logic to + // work properly. + _freezeTracker(tracker); + _trackers.remove(tracker.pointer); + _firstTap = tracker; + } + + void _registerSecondTap(_TapTracker tracker) { + if (_firstTap != null) { + _stopFirstTapUpTimer(); + _freezeTracker(_firstTap!); + _firstTap = null; + } + + _startSecondTapDownTimer(); + GestureBinding.instance.gestureArena.hold(tracker.pointer); + + _secondTap = tracker; + + // TODO + } + + void _clearTrackers() { + _trackers.values.toList().forEach(_reject); + assert(_trackers.isEmpty); + } + + void _freezeTracker(_TapTracker tracker) { + tracker.stopTrackingPointer(_handleEvent); + } + + void _startFirstTapUpTimer() { + _firstTapUpTimer ??= Timer(kDoubleTapTimeout, _reset); + } + + void _startSecondTapDownTimer() { + _secondTapDownTimer ??= Timer(kDoubleTapTimeout, _resolve); + } + + void _stopFirstTapUpTimer() { + if (_firstTapUpTimer != null) { + _firstTapUpTimer!.cancel(); + _firstTapUpTimer = null; + } + } + + void _stopSecondTapDownTimer() { + if (_secondTapDownTimer != null) { + _secondTapDownTimer!.cancel(); + _secondTapDownTimer = null; + } + } + + void _checkCancel() { + if (onHoldDragCancel != null) { + invokeCallback('onHoldDragCancel', onHoldDragCancel!); + } + } + + @override + String get debugDescription => 'double tap'; +} + +class DoubleFinerTapGestureRecognizer extends GestureRecognizer { + DoubleFinerTapGestureRecognizer({ + Object? debugOwner, + Set? supportedDevices, + }) : super( + debugOwner: debugOwner, + supportedDevices: supportedDevices, + ); + + GestureTapDownCallback? onDoubleFinerTapDown; + GestureTapDownCallback? onDoubleFinerTap; + GestureTapCancelCallback? onDoubleFinerTapCancel; + + Timer? _firstTapTimer; + _TapTracker? _firstTap; + + PointerDownEvent? _lastPointerDownEvent; + + var _isStart = false; + + final Set _upTap = {}; + + final Map _trackers = {}; + + @override + bool isPointerAllowed(PointerDownEvent event) { + if (_firstTap == null) { + switch (event.buttons) { + case kPrimaryButton: + if (onDoubleFinerTapDown == null && + onDoubleFinerTap == null && + onDoubleFinerTapCancel == null) { + return false; + } + break; + default: + return false; + } + } + return super.isPointerAllowed(event); + } + + @override + void addAllowedPointer(PointerDownEvent event) { + debugPrint("addAllowedPointer"); + if (_isStart) { + // second + if (onDoubleFinerTapDown != null) { + final TapDownDetails details = TapDownDetails( + globalPosition: event.position, + localPosition: event.localPosition, + kind: getKindForPointer(event.pointer), + ); + invokeCallback( + 'onDoubleFinerTapDown', () => onDoubleFinerTapDown!(details)); + } + } else { + // first tap + _isStart = true; + _lastPointerDownEvent = event; + _startFirstTapDownTimer(); + } + _trackTap(event); + } + + void _trackTap(PointerDownEvent event) { + final _TapTracker tracker = _TapTracker( + event: event, + entry: GestureBinding.instance.gestureArena.add(event.pointer, this), + doubleTapMinTime: kDoubleTapMinTime, + gestureSettings: gestureSettings, + ); + _trackers[event.pointer] = tracker; + // debugPrint("_trackers:$_trackers"); + tracker.startTrackingPointer(_handleEvent, event.transform); + + _registerTap(tracker); + } + + void _handleEvent(PointerEvent event) { + final _TapTracker tracker = _trackers[event.pointer]!; + if (event is PointerUpEvent) { + debugPrint("PointerUpEvent"); + _upTap.add(tracker.pointer); + } else if (event is PointerMoveEvent) { + if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) { + _reject(tracker); + } + } else if (event is PointerCancelEvent) { + _reject(tracker); + } + } + + @override + void acceptGesture(int pointer) {} + + @override + void rejectGesture(int pointer) { + _TapTracker? tracker = _trackers[pointer]; + // If tracker isn't in the list, check if this is the first tap tracker + if (tracker == null && _firstTap != null && _firstTap!.pointer == pointer) { + tracker = _firstTap; + } + // If tracker is still null, we rejected ourselves already + if (tracker != null) { + _reject(tracker); + } + } + + void _reject(_TapTracker tracker) { + _trackers.remove(tracker.pointer); + tracker.entry.resolve(GestureDisposition.rejected); + _freezeTracker(tracker); + if (_firstTap != null) { + if (tracker == _firstTap) { + _reset(); + } else { + _checkCancel(); + if (_trackers.isEmpty) { + _reset(); + } + } + } + } + + @override + void dispose() { + _reset(); + super.dispose(); + } + + void _reset() { + _stopFirstTapUpTimer(); + _firstTap = null; + _clearTrackers(); + } + + void _registerTap(_TapTracker tracker) { + GestureBinding.instance.gestureArena.hold(tracker.pointer); + // Note, order is important below in order for the clear -> reject logic to + // work properly. + } + + void _clearTrackers() { + _trackers.values.toList().forEach(_reject); + assert(_trackers.isEmpty); + } + + void _freezeTracker(_TapTracker tracker) { + tracker.stopTrackingPointer(_handleEvent); + } + + void _startFirstTapDownTimer() { + _firstTapTimer ??= Timer(kDoubleTapTimeout, _timeoutCheck); + } + + void _stopFirstTapUpTimer() { + if (_firstTapTimer != null) { + _firstTapTimer!.cancel(); + _firstTapTimer = null; + } + } + + void _timeoutCheck() { + _isStart = false; + if (_upTap.length == 2) { + _resolve(); + } else { + _reset(); + } + _upTap.clear(); + } + + void _resolve() { + // TODO tap down details + if (onDoubleFinerTap != null) { + onDoubleFinerTap!(TapDownDetails( + kind: _lastPointerDownEvent?.kind, + )); + } + _trackers.forEach((key, value) { + value.entry.resolve(GestureDisposition.accepted); + }); + _reset(); + } + + void _checkCancel() { + if (onDoubleFinerTapCancel != null) { + invokeCallback('onHoldDragCancel', onDoubleFinerTapCancel!); + } + } + + @override + String get debugDescription => 'double tap'; +} + +/// TapTracker helps track individual tap sequences as part of a +/// larger gesture. +class _TapTracker { + _TapTracker({ + required PointerDownEvent event, + required this.entry, + required Duration doubleTapMinTime, + required this.gestureSettings, + }) : pointer = event.pointer, + _initialGlobalPosition = event.position, + initialButtons = event.buttons, + _doubleTapMinTimeCountdown = + _CountdownZoned(duration: doubleTapMinTime); + + final DeviceGestureSettings? gestureSettings; + final int pointer; + final GestureArenaEntry entry; + final Offset _initialGlobalPosition; + final int initialButtons; + final _CountdownZoned _doubleTapMinTimeCountdown; + + bool _isTrackingPointer = false; + + void startTrackingPointer(PointerRoute route, Matrix4? transform) { + if (!_isTrackingPointer) { + _isTrackingPointer = true; + GestureBinding.instance.pointerRouter.addRoute(pointer, route, transform); + } + } + + void stopTrackingPointer(PointerRoute route) { + if (_isTrackingPointer) { + _isTrackingPointer = false; + GestureBinding.instance.pointerRouter.removeRoute(pointer, route); + } + } + + bool isWithinGlobalTolerance(PointerEvent event, double tolerance) { + final Offset offset = event.position - _initialGlobalPosition; + return offset.distance <= tolerance; + } + + bool hasElapsedMinTime() { + return _doubleTapMinTimeCountdown.timeout; + } + + bool hasSameButton(PointerDownEvent event) { + return event.buttons == initialButtons; + } +} + +/// CountdownZoned tracks whether the specified duration has elapsed since +/// creation, honoring [Zone]. +class _CountdownZoned { + _CountdownZoned({required Duration duration}) { + Timer(duration, _onTimeout); + } + + bool _timeout = false; + + bool get timeout => _timeout; + + void _onTimeout() { + _timeout = true; + } +} + +RawGestureDetector getMixinGestureDetector({ + Widget? child, + GestureTapUpCallback? onTapUp, + GestureTapDownCallback? onDoubleTapDown, + GestureDoubleTapCallback? onDoubleTap, + GestureLongPressDownCallback? onLongPressDown, + GestureLongPressCallback? onLongPress, + GestureDragStartCallback? onHoldDragStart, + GestureDragUpdateCallback? onHoldDragUpdate, + GestureDragCancelCallback? onHoldDragCancel, + GestureDragEndCallback? onHoldDragEnd, + GestureTapDownCallback? onDoubleFinerTap, + GestureDragStartCallback? onOneFingerPanStart, + GestureDragUpdateCallback? onOneFingerPanUpdate, + GestureDragEndCallback? onOneFingerPanEnd, + GestureDragCancelCallback? onOneFingerPanCancel, + GestureScaleUpdateCallback? onTwoFingerScaleUpdate, + GestureScaleEndCallback? onTwoFingerScaleEnd, + GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate, +}) { + return RawGestureDetector( + child: child, + gestures: { + // Official + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), (instance) { + instance.onTapUp = onTapUp; + }), + DoubleTapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => DoubleTapGestureRecognizer(), (instance) { + instance + ..onDoubleTapDown = onDoubleTapDown + ..onDoubleTap = onDoubleTap; + }), + LongPressGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(), (instance) { + instance + ..onLongPressDown = onLongPressDown + ..onLongPress = onLongPress; + }), + // Customized + HoldTapMoveGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => HoldTapMoveGestureRecognizer(), + (instance) => instance + ..onHoldDragStart = onHoldDragStart + ..onHoldDragUpdate = onHoldDragUpdate + ..onHoldDragCancel = onHoldDragCancel + ..onHoldDragEnd = onHoldDragEnd), + DoubleFinerTapGestureRecognizer: GestureRecognizerFactoryWithHandlers< + DoubleFinerTapGestureRecognizer>( + () => DoubleFinerTapGestureRecognizer(), (instance) { + instance.onDoubleFinerTap = onDoubleFinerTap; + }), + CustomTouchGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => CustomTouchGestureRecognizer(), (instance) { + instance + ..onOneFingerPanStart = onOneFingerPanStart + ..onOneFingerPanUpdate = onOneFingerPanUpdate + ..onOneFingerPanEnd = onOneFingerPanEnd + ..onOneFingerPanCancel = onOneFingerPanCancel + ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate + ..onTwoFingerScaleEnd = onTwoFingerScaleEnd + ..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate; + }), + }); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/login.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/login.dart new file mode 100644 index 0000000..62ade8e --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/login.dart @@ -0,0 +1,751 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/user_model.dart'; +import 'package:get/get.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../common.dart'; +import './dialog.dart'; + +const kOpSvgList = [ + 'github', + 'gitlab', + 'google', + 'apple', + 'okta', + 'facebook', + 'azure', + 'auth0' +]; + +class _IconOP extends StatelessWidget { + final String op; + final String? icon; + final EdgeInsets margin; + const _IconOP( + {Key? key, + required this.op, + required this.icon, + this.margin = const EdgeInsets.symmetric(horizontal: 4.0)}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final svgFile = + kOpSvgList.contains(op.toLowerCase()) ? op.toLowerCase() : 'default'; + return Container( + margin: margin, + child: icon == null + ? SvgPicture.asset( + 'assets/auth-$svgFile.svg', + width: 20, + ) + : SvgPicture.string( + icon!, + width: 20, + ), + ); + } +} + +class ButtonOP extends StatelessWidget { + final String op; + final RxString curOP; + final String? icon; + final Color primaryColor; + final double height; + final Function() onTap; + + const ButtonOP({ + Key? key, + required this.op, + required this.curOP, + required this.icon, + required this.primaryColor, + required this.height, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final opLabel = { + 'github': 'GitHub', + 'gitlab': 'GitLab' + }[op.toLowerCase()] ?? + toCapitalized(op); + return Row(children: [ + Container( + height: height, + width: 200, + child: Obx(() => ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: curOP.value.isEmpty || curOP.value == op + ? primaryColor + : Colors.grey, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null, + child: Row( + children: [ + SizedBox( + width: 30, + child: _IconOP( + op: op, + icon: icon, + margin: EdgeInsets.only(right: 5), + ), + ), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Center( + child: Text(translate("Continue with {$opLabel}"))), + ), + ), + ], + ))), + ), + ]); + } +} + +class ConfigOP { + final String op; + final String? icon; + ConfigOP({required this.op, required this.icon}); +} + +class WidgetOP extends StatefulWidget { + final ConfigOP config; + final RxString curOP; + final Function(Map) cbLogin; + const WidgetOP({ + Key? key, + required this.config, + required this.curOP, + required this.cbLogin, + }) : super(key: key); + + @override + State createState() { + return _WidgetOPState(); + } +} + +class _WidgetOPState extends State { + Timer? _updateTimer; + String _stateMsg = ''; + String _failedMsg = ''; + String _url = ''; + + @override + void dispose() { + super.dispose(); + _updateTimer?.cancel(); + } + + _beginQueryState() { + _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + _updateState(); + }); + } + + _updateState() { + bind.mainAccountAuthResult().then((result) { + if (result.isEmpty) { + return; + } + final resultMap = jsonDecode(result); + if (resultMap == null) { + return; + } + final String stateMsg = resultMap['state_msg']; + String failedMsg = resultMap['failed_msg']; + final String? url = resultMap['url']; + final bool urlLaunched = (resultMap['url_launched'] as bool?) ?? false; + final authBody = resultMap['auth_body']; + if (_stateMsg != stateMsg || _failedMsg != failedMsg) { + if (_url.isEmpty && url != null && url.isNotEmpty) { + if (!urlLaunched) { + launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + } + _url = url; + } + if (authBody != null) { + _updateTimer?.cancel(); + widget.curOP.value = ''; + widget.cbLogin(authBody as Map); + } + + setState(() { + _stateMsg = stateMsg; + _failedMsg = failedMsg; + if (failedMsg.isNotEmpty) { + widget.curOP.value = ''; + _updateTimer?.cancel(); + } + }); + } + }); + } + + _resetState() { + _stateMsg = ''; + _failedMsg = ''; + _url = ''; + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ButtonOP( + op: widget.config.op, + curOP: widget.curOP, + icon: widget.config.icon, + primaryColor: str2color(widget.config.op, 0x7f), + height: 36, + onTap: () async { + _resetState(); + widget.curOP.value = widget.config.op; + await bind.mainAccountAuth(op: widget.config.op, rememberMe: true); + _beginQueryState(); + }, + ), + Obx(() { + if (widget.curOP.isNotEmpty && + widget.curOP.value != widget.config.op) { + _failedMsg = ''; + } + return Offstage( + offstage: + _failedMsg.isEmpty && widget.curOP.value != widget.config.op, + child: RichText( + text: TextSpan( + text: '$_stateMsg ', + style: + DefaultTextStyle.of(context).style.copyWith(fontSize: 12), + children: [ + TextSpan( + text: _failedMsg, + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 14, + color: Colors.red, + ), + ), + ], + ), + ), + ); + }), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: const SizedBox( + height: 5.0, + ), + ), + ), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 20), + child: ElevatedButton( + onPressed: () { + widget.curOP.value = ''; + _updateTimer?.cancel(); + _resetState(); + bind.mainAccountAuthCancel(); + }, + child: Text( + translate('Cancel'), + style: TextStyle(fontSize: 15), + ), + ), + ), + ), + ), + ], + ); + } +} + +class LoginWidgetOP extends StatelessWidget { + final List ops; + final RxString curOP; + final Function(Map) cbLogin; + + LoginWidgetOP({ + Key? key, + required this.ops, + required this.curOP, + required this.cbLogin, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var children = ops + .map((op) => [ + WidgetOP( + config: op, + curOP: curOP, + cbLogin: cbLogin, + ), + const Divider( + indent: 5, + endIndent: 5, + ) + ]) + .expand((i) => i) + .toList(); + if (children.isNotEmpty) { + children.removeLast(); + } + return SingleChildScrollView( + child: Container( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: children, + ))); + } +} + +class LoginWidgetUserPass extends StatelessWidget { + final TextEditingController username; + final TextEditingController pass; + final String? usernameMsg; + final String? passMsg; + final bool isInProgress; + final RxString curOP; + final Function() onLogin; + final FocusNode? userFocusNode; + const LoginWidgetUserPass({ + Key? key, + this.userFocusNode, + required this.username, + required this.pass, + required this.usernameMsg, + required this.passMsg, + required this.isInProgress, + required this.curOP, + required this.onLogin, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8.0), + DialogTextField( + title: translate(DialogTextField.kUsernameTitle), + controller: username, + focusNode: userFocusNode, + prefixIcon: DialogTextField.kUsernameIcon, + errorText: usernameMsg), + PasswordWidget( + controller: pass, + autoFocus: false, + reRequestFocus: true, + errorText: passMsg, + ), + // NOT use Offstage to wrap LinearProgressIndicator + if (isInProgress) const LinearProgressIndicator(), + const SizedBox(height: 12.0), + FittedBox( + child: + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container( + height: 38, + width: 200, + child: Obx(() => ElevatedButton( + child: Text( + translate('Login'), + style: TextStyle(fontSize: 16), + ), + onPressed: + curOP.value.isEmpty || curOP.value == 'rustdesk' + ? () { + onLogin(); + } + : null, + )), + ), + ])), + ], + )); + } +} + +const kAuthReqTypeOidc = 'oidc/'; + +// call this directly +Future loginDialog() async { + var username = + TextEditingController(text: UserModel.getLocalUserInfo()?['name'] ?? ''); + var password = TextEditingController(); + final userFocusNode = FocusNode()..requestFocus(); + Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus()); + + String? usernameMsg; + String? passwordMsg; + var isInProgress = false; + final RxString curOP = ''.obs; + // Track hover state for the close icon + bool isCloseHovered = false; + + final loginOptions = [].obs; + Future.delayed(Duration.zero, () async { + loginOptions.value = await UserModel.queryOidcLoginOptions(); + }); + + final res = await gFFI.dialogManager.show((setState, close, context) { + username.addListener(() { + if (usernameMsg != null) { + setState(() => usernameMsg = null); + } + }); + + password.addListener(() { + if (passwordMsg != null) { + setState(() => passwordMsg = null); + } + }); + + onDialogCancel() { + isInProgress = false; + close(false); + } + + handleLoginResponse(LoginResponse resp, bool storeIfAccessToken, + void Function([dynamic])? close) async { + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + if (storeIfAccessToken) { + await bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + await bind.mainSetLocalOption( + key: 'user_info', value: jsonEncode(resp.user ?? {})); + } + if (close != null) { + close(true); + } + return; + } + break; + case HttpType.kAuthResTypeEmailCheck: + bool? isEmailVerification; + if (resp.tfa_type == null || + resp.tfa_type == HttpType.kAuthResTypeEmailCheck) { + isEmailVerification = true; + } else if (resp.tfa_type == HttpType.kAuthResTypeTfaCheck) { + isEmailVerification = false; + } else { + passwordMsg = "Failed, bad tfa type from server"; + } + if (isEmailVerification != null) { + if (isMobile) { + if (close != null) close(null); + verificationCodeDialog( + resp.user, resp.secret, isEmailVerification); + } else { + setState(() => isInProgress = false); + // Workaround for web, close the dialog first, then show the verification code dialog. + // Otherwise, the text field will keep selecting the text and we can't input the code. + // Not sure why this happens. + if (isWeb && close != null) close(null); + final res = await verificationCodeDialog( + resp.user, resp.secret, isEmailVerification); + if (res == true) { + if (!isWeb && close != null) close(false); + return; + } + } + } + break; + default: + passwordMsg = "Failed, bad response from server"; + break; + } + } + + onLogin() async { + // validate + if (username.text.isEmpty) { + setState(() => usernameMsg = translate('Username missed')); + return; + } + if (password.text.isEmpty) { + setState(() => passwordMsg = translate('Password missed')); + return; + } + curOP.value = 'rustdesk'; + setState(() => isInProgress = true); + try { + final resp = await gFFI.userModel.login(LoginRequest( + username: username.text, + password: password.text, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: true, + type: HttpType.kAuthReqTypeAccount)); + await handleLoginResponse(resp, true, close); + } on RequestException catch (err) { + passwordMsg = translate(err.cause); + } catch (err) { + passwordMsg = "Unknown Error: $err"; + } + curOP.value = ''; + setState(() => isInProgress = false); + } + + thirdAuthWidget() => Obx(() { + return Offstage( + offstage: loginOptions.isEmpty, + child: Column( + children: [ + const SizedBox( + height: 8.0, + ), + Center( + child: Text( + translate('or'), + style: TextStyle(fontSize: 16), + )), + const SizedBox( + height: 8.0, + ), + LoginWidgetOP( + ops: loginOptions + .map((e) => ConfigOP(op: e['name'], icon: e['icon'])) + .toList(), + curOP: curOP, + cbLogin: (Map authBody) async { + LoginResponse? resp; + try { + // access_token is already stored in the rust side. + resp = + gFFI.userModel.getLoginResponseFromAuthBody(authBody); + } catch (e) { + debugPrint( + 'Failed to parse oidc login body: "$authBody"'); + } + close(true); + + if (resp != null) { + handleLoginResponse(resp, false, null); + } + }, + ), + ], + ), + ); + }); + + final title = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate('Login'), + ).marginOnly(top: MyTheme.dialogPadding), + MouseRegion( + onEnter: (_) => setState(() => isCloseHovered = true), + onExit: (_) => setState(() => isCloseHovered = false), + child: InkWell( + child: Icon( + Icons.close, + size: 25, + // No need to handle the branch of null. + // Because we can ensure the color is not null when debug. + color: isCloseHovered + ? Colors.white + : Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.55), + ), + onTap: onDialogCancel, + hoverColor: Colors.red, + borderRadius: BorderRadius.circular(5), + ), + ).marginOnly(top: 10, right: 15), + ], + ); + final titlePadding = EdgeInsets.fromLTRB(MyTheme.dialogPadding, 0, 0, 0); + + return CustomAlertDialog( + title: title, + titlePadding: titlePadding, + contentBoxConstraints: BoxConstraints(minWidth: 400), + content: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + height: 8.0, + ), + LoginWidgetUserPass( + username: username, + pass: password, + usernameMsg: usernameMsg, + passMsg: passwordMsg, + isInProgress: isInProgress, + curOP: curOP, + onLogin: onLogin, + userFocusNode: userFocusNode, + ), + thirdAuthWidget(), + ], + ), + onCancel: onDialogCancel, + onSubmit: onLogin, + ); + }); + + if (res != null) { + await UserModel.updateOtherModels(); + } + + return res; +} + +Future verificationCodeDialog( + UserPayload? user, String? secret, bool isEmailVerification) async { + var autoLogin = true; + var isInProgress = false; + String? errorText; + + final code = TextEditingController(); + + final res = await gFFI.dialogManager.show((setState, close, context) { + void onVerify() async { + setState(() => isInProgress = true); + + try { + final resp = await gFFI.userModel.login(LoginRequest( + verificationCode: code.text, + tfaCode: isEmailVerification ? null : code.text, + secret: secret, + username: user?.name, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: autoLogin, + type: HttpType.kAuthReqTypeEmailCode)); + + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + await bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + close(true); + return; + } + break; + default: + errorText = "Failed, bad response from server"; + break; + } + } on RequestException catch (err) { + errorText = translate(err.cause); + } catch (err) { + errorText = "Unknown Error: $err"; + } + + setState(() => isInProgress = false); + } + + final codeField = isEmailVerification + ? DialogEmailCodeField( + controller: code, + errorText: errorText, + readyCallback: onVerify, + onChanged: () => errorText = null, + ) + : Dialog2FaField( + controller: code, + errorText: errorText, + readyCallback: onVerify, + onChanged: () => errorText = null, + ); + + getOnSubmit() => codeField.isReady ? onVerify : null; + + return CustomAlertDialog( + title: Text(translate("Verification code")), + contentBoxConstraints: BoxConstraints(maxWidth: 300), + content: Column( + children: [ + Offstage( + offstage: !isEmailVerification || user?.email == null, + child: TextField( + decoration: InputDecoration( + labelText: "Email", prefixIcon: Icon(Icons.email)), + readOnly: true, + controller: TextEditingController(text: user?.email), + ).workaroundFreezeLinuxMint()), + isEmailVerification ? const SizedBox(height: 8) : const Offstage(), + codeField, + /* + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Row(children: [ + Expanded(child: Text(translate("Trust this device"))) + ]), + value: trustThisDevice, + onChanged: (v) { + if (v == null) return; + setState(() => trustThisDevice = !trustThisDevice); + }, + ), + */ + // NOT use Offstage to wrap LinearProgressIndicator + if (isInProgress) const LinearProgressIndicator(), + ], + ), + onCancel: close, + onSubmit: getOnSubmit(), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Verify", onPressed: getOnSubmit()), + ]); + }); + // For verification code, desktop update other models in login dialog, mobile need to close login dialog first, + // otherwise the soft keyboard will jump out on each key press, so mobile update in verification code dialog. + if (isMobile && res == true) { + await UserModel.updateOtherModels(); + } + + return res; +} + +void logOutConfirmDialog() { + gFFI.dialogManager.show((setState, close, context) { + submit() { + close(); + gFFI.userModel.logOut(); + } + + return CustomAlertDialog( + content: Text(translate("logout_tip")), + actions: [ + dialogButton(translate("Cancel"), onPressed: close, isOutline: true), + dialogButton(translate("OK"), onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/my_group.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/my_group.dart new file mode 100644 index 0000000..74ce34e --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/my_group.dart @@ -0,0 +1,309 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/common/widgets/login.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +class MyGroup extends StatefulWidget { + final EdgeInsets? menuPadding; + const MyGroup({Key? key, this.menuPadding}) : super(key: key); + + @override + State createState() { + return _MyGroupState(); + } +} + +class _MyGroupState extends State { + RxBool get isSelectedDeviceGroup => gFFI.groupModel.isSelectedDeviceGroup; + RxString get selectedAccessibleItemName => + gFFI.groupModel.selectedAccessibleItemName; + RxString get searchAccessibleItemNameText => + gFFI.groupModel.searchAccessibleItemNameText; + static TextEditingController searchUserController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Obx(() { + if (!gFFI.userModel.isLogin) { + return Center( + child: ElevatedButton( + onPressed: loginDialog, child: Text(translate("Login")))); + } else if (gFFI.userModel.networkError.isNotEmpty) { + return netWorkErrorWidget(); + } else if (gFFI.groupModel.groupLoading.value && gFFI.groupModel.emtpy) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return Column( + children: [ + buildErrorBanner(context, + loading: gFFI.groupModel.groupLoading, + err: gFFI.groupModel.groupLoadError, + retry: null, + close: () => gFFI.groupModel.groupLoadError.value = ''), + Expanded( + child: Obx(() => stateGlobal.isPortrait.isTrue + ? _buildPortrait() + : _buildLandscape())), + ], + ); + }); + } + + Widget _buildLandscape() { + return Row( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: Theme.of(context).colorScheme.background)), + child: Container( + width: 150, + height: double.infinity, + child: Column( + children: [ + _buildLeftHeader(), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + child: _buildLeftList(), + ), + ) + ], + ), + ), + ).marginOnly(right: 12.0), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + )), + ) + ], + ); + } + + Widget _buildPortrait() { + return Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: + Border.all(color: Theme.of(context).colorScheme.background)), + child: Container( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildLeftHeader(), + Container( + width: double.infinity, + child: _buildLeftList(), + ) + ], + ), + ), + ).marginOnly(bottom: 12.0), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + )), + ) + ], + ); + } + + Widget _buildLeftHeader() { + final fontSize = 14.0; + return Row( + children: [ + Expanded( + child: TextField( + controller: searchUserController, + onChanged: (value) { + searchAccessibleItemNameText.value = value; + selectedAccessibleItemName.value = ''; + }, + textAlignVertical: TextAlignVertical.center, + style: TextStyle(fontSize: fontSize), + decoration: InputDecoration( + filled: false, + prefixIcon: Icon( + Icons.search_rounded, + color: Theme.of(context).hintColor, + ).paddingOnly(top: 2), + hintText: translate("Search"), + hintStyle: TextStyle(fontSize: fontSize), + border: InputBorder.none, + isDense: true, + ), + ).workaroundFreezeLinuxMint()), + ], + ); + } + + Widget _buildLeftList() { + return Obx(() { + final userItems = gFFI.groupModel.users.where((p0) { + if (searchAccessibleItemNameText.isNotEmpty) { + final search = searchAccessibleItemNameText.value.toLowerCase(); + return p0.name.toLowerCase().contains(search) || + p0.displayNameOrName.toLowerCase().contains(search); + } + return true; + }).toList(); + // Count occurrences of each displayNameOrName to detect duplicates + final displayNameCount = {}; + for (final u in userItems) { + final dn = u.displayNameOrName; + displayNameCount[dn] = (displayNameCount[dn] ?? 0) + 1; + } + final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) { + if (searchAccessibleItemNameText.isNotEmpty) { + return p0.name + .toLowerCase() + .contains(searchAccessibleItemNameText.value.toLowerCase()); + } + return true; + }).toList(); + listView(bool isPortrait) => ListView.builder( + shrinkWrap: isPortrait, + itemCount: deviceGroupItems.length + userItems.length, + itemBuilder: (context, index) => index < deviceGroupItems.length + ? _buildDeviceGroupItem(deviceGroupItems[index]) + : _buildUserItem(userItems[index - deviceGroupItems.length], + displayNameCount)); + var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); + return Obx(() => stateGlobal.isPortrait.isFalse + ? listView(false) + : LimitedBox(maxHeight: maxHeight, child: listView(true))); + }); + } + + Widget _buildUserItem(UserPayload user, Map displayNameCount) { + final username = user.name; + final dn = user.displayNameOrName; + final isDuplicate = (displayNameCount[dn] ?? 0) > 1; + final displayName = + isDuplicate && user.displayName.trim().isNotEmpty + ? '${user.displayName} (@$username)' + : dn; + return InkWell(onTap: () { + isSelectedDeviceGroup.value = false; + if (selectedAccessibleItemName.value != username) { + selectedAccessibleItemName.value = username; + } else { + selectedAccessibleItemName.value = ''; + } + }, child: Obx( + () { + bool selected = !isSelectedDeviceGroup.value && + selectedAccessibleItemName.value == username; + final isMe = username == gFFI.userModel.userName.value; + final colorMe = MyTheme.color(context).me!; + return Container( + decoration: BoxDecoration( + color: selected ? MyTheme.color(context).highlight : null, + border: Border( + bottom: BorderSide( + width: 0.7, + color: Theme.of(context).dividerColor.withOpacity(0.1))), + ), + child: Container( + child: Row( + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: str2color(username, 0xAF), + shape: BoxShape.circle, + ), + child: Align( + alignment: Alignment.center, + child: Center( + child: Text( + displayName.characters.first.toUpperCase(), + style: TextStyle(color: Colors.white), + textAlign: TextAlign.center, + ), + ), + ), + ).marginOnly(right: 4), + if (isMe) Flexible(child: Text(displayName)), + if (isMe) + Flexible( + child: Container( + margin: EdgeInsets.only(left: 5), + padding: EdgeInsets.symmetric(horizontal: 3, vertical: 1), + decoration: BoxDecoration( + color: colorMe.withAlpha(20), + borderRadius: BorderRadius.all(Radius.circular(2)), + border: Border.all(color: colorMe.withAlpha(100))), + child: Text( + translate('Me'), + style: TextStyle( + color: colorMe.withAlpha(200), fontSize: 12), + ), + ), + ), + if (!isMe) Expanded(child: Text(displayName)), + ], + ).paddingSymmetric(vertical: 4), + ), + ); + }, + )).marginSymmetric(horizontal: 12).marginOnly(bottom: 6); + } + + Widget _buildDeviceGroupItem(DeviceGroupPayload deviceGroup) { + final name = deviceGroup.name; + return InkWell(onTap: () { + isSelectedDeviceGroup.value = true; + if (selectedAccessibleItemName.value != name) { + selectedAccessibleItemName.value = name; + } else { + selectedAccessibleItemName.value = ''; + } + }, child: Obx( + () { + bool selected = isSelectedDeviceGroup.value && + selectedAccessibleItemName.value == name; + return Container( + decoration: BoxDecoration( + color: selected ? MyTheme.color(context).highlight : null, + border: Border( + bottom: BorderSide( + width: 0.7, + color: Theme.of(context).dividerColor.withOpacity(0.1))), + ), + child: Container( + child: Row( + children: [ + Container( + width: 20, + height: 20, + child: Icon(IconFont.deviceGroupOutline, + color: MyTheme.accent, size: 19), + ).marginOnly(right: 4), + Expanded(child: Text(name)), + ], + ).paddingSymmetric(vertical: 4), + ), + ); + }, + )).marginSymmetric(horizontal: 12).marginOnly(bottom: 6); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/overlay.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/overlay.dart new file mode 100644 index 0000000..3fb6361 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/overlay.dart @@ -0,0 +1,674 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; + +import '../../consts.dart'; +import '../../desktop/widgets/tabbar_widget.dart'; +import '../../models/chat_model.dart'; +import '../../models/model.dart'; +import 'chat_page.dart'; + +class DraggableChatWindow extends StatelessWidget { + const DraggableChatWindow( + {Key? key, + this.position = Offset.zero, + required this.width, + required this.height, + required this.chatModel}) + : super(key: key); + + final Offset position; + final double width; + final double height; + final ChatModel chatModel; + + @override + Widget build(BuildContext context) { + if (draggablePositions.chatWindow.isInvalid()) { + draggablePositions.chatWindow.update(position); + } + return isIOS + ? IOSDraggable( + position: draggablePositions.chatWindow, + chatModel: chatModel, + width: width, + height: height, + builder: (context) { + return Column( + children: [ + _buildMobileAppBar(context), + Expanded( + child: ChatPage(chatModel: chatModel), + ), + ], + ); + }, + ) + : Draggable( + checkKeyboard: true, + checkScreenSize: true, + position: draggablePositions.chatWindow, + width: width, + height: height, + chatModel: chatModel, + builder: (context, onPanUpdate) { + final child = Scaffold( + resizeToAvoidBottomInset: false, + appBar: CustomAppBar( + onPanUpdate: onPanUpdate, + appBar: (isDesktop || isWebDesktop) + ? _buildDesktopAppBar(context) + : _buildMobileAppBar(context), + ), + body: ChatPage(chatModel: chatModel), + ); + return Container( + decoration: + BoxDecoration(border: Border.all(color: MyTheme.border)), + child: child); + }); + } + + Widget _buildMobileAppBar(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.primary, + height: 50, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Text( + translate("Chat"), + style: const TextStyle( + color: Colors.white, + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 20), + )), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + chatModel.hideChatWindowOverlay(); + }, + icon: const Icon( + Icons.keyboard_arrow_down, + color: Colors.white, + )), + IconButton( + onPressed: () { + chatModel.hideChatWindowOverlay(); + chatModel.hideChatIconOverlay(); + }, + icon: const Icon( + Icons.close, + color: Colors.white, + )) + ], + ) + ], + ), + ); + } + + Widget _buildDesktopAppBar(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).hintColor.withOpacity(0.4)))), + height: 38, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Obx(() => Opacity( + opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, + child: Row(children: [ + Icon(Icons.chat_bubble_outline, + size: 20, color: Theme.of(context).colorScheme.primary), + SizedBox(width: 6), + Text(translate("Chat")) + ])))), + Padding( + padding: EdgeInsets.all(2), + child: ActionIcon( + message: 'Close', + icon: IconFont.close, + onTap: chatModel.hideChatWindowOverlay, + isClose: true, + boxSize: 32, + )) + ], + ), + ); + } +} + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + final GestureDragUpdateCallback onPanUpdate; + final Widget appBar; + + const CustomAppBar( + {Key? key, required this.onPanUpdate, required this.appBar}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector(onPanUpdate: onPanUpdate, child: appBar); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +/// floating buttons of back/home/recent actions for android +class DraggableMobileActions extends StatelessWidget { + DraggableMobileActions( + {this.onBackPressed, + this.onRecentPressed, + this.onHomePressed, + this.onHidePressed, + required this.position, + required this.width, + required this.height, + required this.scale}); + + final double scale; + final DraggableKeyPosition position; + final double width; + final double height; + final VoidCallback? onBackPressed; + final VoidCallback? onHomePressed; + final VoidCallback? onRecentPressed; + final VoidCallback? onHidePressed; + + @override + Widget build(BuildContext context) { + return Draggable( + position: position, + width: scale * width, + height: scale * height, + builder: (_, onPanUpdate) { + return GestureDetector( + onPanUpdate: onPanUpdate, + child: Card( + color: Colors.transparent, + shadowColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: MyTheme.accent.withOpacity(0.4), + borderRadius: + BorderRadius.all(Radius.circular(15 * scale))), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton( + color: Colors.white, + onPressed: onBackPressed, + splashRadius: kDesktopIconButtonSplashRadius, + icon: const Icon(Icons.arrow_back), + iconSize: 24 * scale), + IconButton( + color: Colors.white, + onPressed: onHomePressed, + splashRadius: kDesktopIconButtonSplashRadius, + icon: const Icon(Icons.home), + iconSize: 24 * scale), + IconButton( + color: Colors.white, + onPressed: onRecentPressed, + splashRadius: kDesktopIconButtonSplashRadius, + icon: const Icon(Icons.more_horiz), + iconSize: 24 * scale), + const VerticalDivider( + width: 0, + thickness: 2, + indent: 10, + endIndent: 10, + ), + IconButton( + color: Colors.white, + onPressed: onHidePressed, + splashRadius: kDesktopIconButtonSplashRadius, + icon: const Icon(Icons.keyboard_arrow_down), + iconSize: 24 * scale), + ], + ), + ))); + }); + } +} + +class DraggableKeyPosition { + final String key; + Offset _pos; + late Debouncer _debouncerStore; + DraggableKeyPosition(this.key) + : _pos = DraggablePositions.kInvalidDraggablePosition; + + get pos => _pos; + + _loadPosition(String k) { + final value = bind.getLocalFlutterOption(k: k); + if (value.isNotEmpty) { + final parts = value.split(','); + if (parts.length == 2) { + return Offset(double.parse(parts[0]), double.parse(parts[1])); + } + } + return DraggablePositions.kInvalidDraggablePosition; + } + + load() { + _pos = _loadPosition(key); + _debouncerStore = Debouncer(const Duration(milliseconds: 500), + onChanged: (v) => _store(), initialValue: 0); + } + + update(Offset pos) { + _pos = pos; + _triggerStore(); + } + + // Adjust position to keep it in the screen + // Only used for desktop and web desktop + tryAdjust(double w, double h, double scale) { + final size = MediaQuery.of(Get.context!).size; + w = w * scale; + h = h * scale; + double x = _pos.dx; + double y = _pos.dy; + if (x + w > size.width) { + x = size.width - w; + } + final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0; + if (y + h > (size.height - tabBarHeight)) { + y = size.height - tabBarHeight - h; + } + if (x < 0) { + x = 0; + } + if (y < 0) { + y = 0; + } + if (x != _pos.dx || y != _pos.dy) { + update(Offset(x, y)); + } + } + + isInvalid() { + return _pos == DraggablePositions.kInvalidDraggablePosition; + } + + _triggerStore() => _debouncerStore.value = _debouncerStore.value + 1; + _store() { + bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}'); + } +} + +class DraggablePositions { + static const kChatWindow = 'draggablePositionChat'; + static const kMobileActions = 'draggablePositionMobile'; + static const kIOSDraggable = 'draggablePositionIOS'; + + static const kInvalidDraggablePosition = Offset(-999999, -999999); + final chatWindow = DraggableKeyPosition(kChatWindow); + final mobileActions = DraggableKeyPosition(kMobileActions); + final iOSDraggable = DraggableKeyPosition(kIOSDraggable); + + load() { + chatWindow.load(); + mobileActions.load(); + iOSDraggable.load(); + } +} + +DraggablePositions draggablePositions = DraggablePositions(); + +class Draggable extends StatefulWidget { + Draggable( + {Key? key, + this.checkKeyboard = false, + this.checkScreenSize = false, + required this.position, + required this.width, + required this.height, + this.chatModel, + required this.builder}) + : super(key: key); + + final bool checkKeyboard; + final bool checkScreenSize; + final DraggableKeyPosition position; + final double width; + final double height; + final ChatModel? chatModel; + final Widget Function(BuildContext, GestureDragUpdateCallback) builder; + + @override + State createState() => _DraggableState(chatModel); +} + +class _DraggableState extends State { + late ChatModel? _chatModel; + bool _keyboardVisible = false; + double _saveHeight = 0; + double _lastBottomHeight = 0; + + _DraggableState(ChatModel? chatModel) { + _chatModel = chatModel; + } + + get position => widget.position.pos; + + void onPanUpdate(DragUpdateDetails d) { + final offset = d.delta; + final size = MediaQuery.of(context).size; + double x = 0; + double y = 0; + + if (position.dx + offset.dx + widget.width > size.width) { + x = size.width - widget.width; + } else if (position.dx + offset.dx < 0) { + x = 0; + } else { + x = position.dx + offset.dx; + } + + if (position.dy + offset.dy + widget.height > size.height) { + y = size.height - widget.height; + } else if (position.dy + offset.dy < 0) { + y = 0; + } else { + y = position.dy + offset.dy; + } + setState(() { + widget.position.update(Offset(x, y)); + }); + _chatModel?.setChatWindowPosition(position); + } + + checkScreenSize() { + // Ensure the draggable always stays within current screen bounds + widget.position.tryAdjust(widget.width, widget.height, 1); + } + + checkKeyboard() { + final bottomHeight = MediaQuery.of(context).viewInsets.bottom; + final currentVisible = bottomHeight != 0; + + // save + if (!_keyboardVisible && currentVisible) { + _saveHeight = position.dy; + } + + // reset + if (_lastBottomHeight > 0 && bottomHeight == 0) { + setState(() { + widget.position.update(Offset(position.dx, _saveHeight)); + }); + } + + // onKeyboardVisible + if (_keyboardVisible && currentVisible) { + final sumHeight = bottomHeight + widget.height; + final contextHeight = MediaQuery.of(context).size.height; + if (sumHeight + position.dy > contextHeight) { + final y = contextHeight - sumHeight; + setState(() { + widget.position.update(Offset(position.dx, y)); + }); + } + } + + _keyboardVisible = currentVisible; + _lastBottomHeight = bottomHeight; + } + + @override + Widget build(BuildContext context) { + if (widget.checkKeyboard) { + checkKeyboard(); + } + if (widget.checkScreenSize) { + checkScreenSize(); + } + return Stack(children: [ + Positioned( + top: position.dy, + left: position.dx, + width: widget.width, + height: widget.height, + child: widget.builder(context, onPanUpdate)) + ]); + } +} + +class IOSDraggable extends StatefulWidget { + const IOSDraggable( + {Key? key, + this.chatModel, + required this.position, + required this.width, + required this.height, + required this.builder}) + : super(key: key); + + final DraggableKeyPosition position; + final ChatModel? chatModel; + final double width; + final double height; + final Widget Function(BuildContext) builder; + + @override + IOSDraggableState createState() => + IOSDraggableState(chatModel, width, height); +} + +class IOSDraggableState extends State { + late ChatModel? _chatModel; + late double _width; + late double _height; + bool _keyboardVisible = false; + double _saveHeight = 0; + double _lastBottomHeight = 0; + + IOSDraggableState(ChatModel? chatModel, double w, double h) { + _chatModel = chatModel; + _width = w; + _height = h; + } + + DraggableKeyPosition get position => widget.position; + + checkKeyboard() { + final bottomHeight = MediaQuery.of(context).viewInsets.bottom; + final currentVisible = bottomHeight != 0; + + // save + if (!_keyboardVisible && currentVisible) { + _saveHeight = position.pos.dy; + } + + // reset + if (_lastBottomHeight > 0 && bottomHeight == 0) { + setState(() { + position.update(Offset(position.pos.dx, _saveHeight)); + }); + } + + // onKeyboardVisible + if (_keyboardVisible && currentVisible) { + final sumHeight = bottomHeight + _height; + final contextHeight = MediaQuery.of(context).size.height; + if (sumHeight + position.pos.dy > contextHeight) { + final y = contextHeight - sumHeight; + setState(() { + position.update(Offset(position.pos.dx, y)); + }); + } + } + + _keyboardVisible = currentVisible; + _lastBottomHeight = bottomHeight; + } + + @override + void initState() { + super.initState(); + position.tryAdjust(_width, _height, 1); + } + + @override + Widget build(BuildContext context) { + checkKeyboard(); + return Stack( + children: [ + Positioned( + left: position.pos.dx, + top: position.pos.dy, + child: GestureDetector( + onPanUpdate: (details) { + setState(() { + position.update(position.pos + details.delta); + }); + _chatModel?.setChatWindowPosition(position.pos); + }, + child: Material( + child: Container( + width: _width, + height: _height, + decoration: + BoxDecoration(border: Border.all(color: MyTheme.border)), + child: widget.builder(context), + ), + ), + ), + ), + ], + ); + } +} + +class QualityMonitor extends StatelessWidget { + final QualityMonitorModel qualityMonitorModel; + QualityMonitor(this.qualityMonitorModel); + + Widget _row(String info, String? value, {Color? rightColor}) { + return Row( + children: [ + Expanded( + flex: 8, + child: AutoSizeText(info, + style: TextStyle(color: Color.fromARGB(255, 210, 210, 210)), + textAlign: TextAlign.right, + maxLines: 1)), + Spacer(flex: 1), + Expanded( + flex: 8, + child: AutoSizeText(value ?? '', + style: TextStyle(color: rightColor ?? Colors.white), + maxLines: 1)), + ], + ); + } + + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: qualityMonitorModel, + child: Consumer( + builder: (context, qualityMonitorModel, child) => qualityMonitorModel + .show + ? Container( + constraints: BoxConstraints(maxWidth: 200), + padding: const EdgeInsets.all(8), + color: MyTheme.canvasColor.withAlpha(150), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _row("Speed", qualityMonitorModel.data.speed ?? '-'), + _row("FPS", qualityMonitorModel.data.fps ?? '-'), + // let delay be 0 if fps is 0 + _row( + "Delay", + "${qualityMonitorModel.data.delay == null ? '-' : (qualityMonitorModel.data.fps ?? "").replaceAll(' ', '').replaceAll('0', '').isEmpty ? 0 : qualityMonitorModel.data.delay}ms", + rightColor: Colors.green), + _row("Target Bitrate", + "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"), + _row( + "Codec", qualityMonitorModel.data.codecFormat ?? '-'), + _row("Chroma", qualityMonitorModel.data.chroma ?? '-'), + ], + ), + ) + : const SizedBox.shrink())); +} + +class BlockableOverlayState extends OverlayKeyState { + final _middleBlocked = false.obs; + + VoidCallback? onMiddleBlockedClick; // to-do use listener + + RxBool get middleBlocked => _middleBlocked; + + void addMiddleBlockedListener(void Function(bool) cb) { + _middleBlocked.listen(cb); + } + + void setMiddleBlocked(bool blocked) { + if (blocked != _middleBlocked.value) { + _middleBlocked.value = blocked; + } + } + + void applyFfi(FFI ffi) { + ffi.dialogManager.setOverlayState(this); + ffi.chatModel.setOverlayState(this); + // make remote page penetrable automatically, effective for chat over remote + onMiddleBlockedClick = () { + setMiddleBlocked(false); + }; + } +} + +class BlockableOverlay extends StatelessWidget { + final Widget underlying; + final List? upperLayer; + + final BlockableOverlayState state; + + BlockableOverlay( + {required this.underlying, required this.state, this.upperLayer}); + + @override + Widget build(BuildContext context) { + final initialEntries = [ + OverlayEntry(builder: (_) => underlying), + + /// middle layer + OverlayEntry( + builder: (context) => Obx(() => Listener( + onPointerDown: (_) { + state.onMiddleBlockedClick?.call(); + }, + child: Container( + color: + state.middleBlocked.value ? Colors.transparent : null)))), + ]; + + if (upperLayer != null) { + initialEntries.addAll(upperLayer!); + } + + /// set key + return Overlay(key: state.key, initialEntries: initialEntries); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/peer_card.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/peer_card.dart new file mode 100644 index 0000000..1f9f3ed --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/peer_card.dart @@ -0,0 +1,1581 @@ +import 'package:bot_toast/bot_toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; + +import '../../common.dart'; +import '../../common/formatter/id_formatter.dart'; +import '../../models/peer_model.dart'; +import '../../models/platform_model.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; +import '../../desktop/widgets/popup_menu.dart'; +import 'dart:math' as math; + +typedef PopupMenuEntryBuilder = Future>> + Function(BuildContext); + +enum PeerUiType { grid, tile, list } + +final peerCardUiType = PeerUiType.grid.obs; + +bool? hideUsernameOnCard; + +class _PeerCard extends StatefulWidget { + final Peer peer; + final PeerTabIndex tab; + final Function(BuildContext, String) connect; + final PopupMenuEntryBuilder popupMenuEntryBuilder; + + const _PeerCard( + {required this.peer, + required this.tab, + required this.connect, + required this.popupMenuEntryBuilder, + Key? key}) + : super(key: key); + + @override + _PeerCardState createState() => _PeerCardState(); +} + +/// State for the connection page. +class _PeerCardState extends State<_PeerCard> + with AutomaticKeepAliveClientMixin { + var _menuPos = RelativeRect.fill; + final double _cardRadius = 16; + final double _tileRadius = 5; + final double _borderWidth = 2; + + @override + Widget build(BuildContext context) { + super.build(context); + return Obx(() => + stateGlobal.isPortrait.isTrue ? _buildPortrait() : _buildLandscape()); + } + + Widget gestureDetector({required Widget child}) { + final PeerTabModel peerTabModel = Provider.of(context); + final peer = super.widget.peer; + return GestureDetector( + onDoubleTap: peerTabModel.multiSelectionMode + ? null + : () => widget.connect(context, peer.id), + onTap: () { + if (peerTabModel.multiSelectionMode) { + peerTabModel.select(peer); + } else { + if (isMobile) { + widget.connect(context, peer.id); + } else { + peerTabModel.select(peer); + } + } + }, + onLongPress: () => peerTabModel.select(peer), + child: child); + } + + Widget _buildPortrait() { + final peer = super.widget.peer; + return Card( + margin: EdgeInsets.symmetric(horizontal: 2), + child: gestureDetector( + child: Container( + padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), + child: _buildPeerTile(context, peer, null)), + )); + } + + Widget _buildLandscape() { + final peer = super.widget.peer; + var deco = Rx( + BoxDecoration( + border: Border.all(color: Colors.transparent, width: _borderWidth), + borderRadius: BorderRadius.circular( + peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius, + ), + ), + ); + return MouseRegion( + onEnter: (evt) { + deco.value = BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: _borderWidth), + borderRadius: BorderRadius.circular( + peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius, + ), + ); + }, + onExit: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: Colors.transparent, width: _borderWidth), + borderRadius: BorderRadius.circular( + peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius, + ), + ); + }, + child: gestureDetector( + child: Obx(() => peerCardUiType.value == PeerUiType.grid + ? _buildPeerCard(context, peer, deco) + : _buildPeerTile(context, peer, deco))), + ); + } + + bool _showNote(Peer peer) { + return peerTabShowNote(widget.tab) && peer.note.isNotEmpty; + } + + makeChild(bool isPortrait, Peer peer) { + final name = hideUsernameOnCard == true + ? peer.hostname + : '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; + final greyStyle = TextStyle( + fontSize: 11, + color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); + final showNote = _showNote(peer); + + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + borderRadius: isPortrait + ? BorderRadius.circular(_tileRadius) + : BorderRadius.only( + topLeft: Radius.circular(_tileRadius), + bottomLeft: Radius.circular(_tileRadius), + ), + ), + alignment: Alignment.center, + width: isPortrait ? 50 : 42, + height: isPortrait ? 50 : null, + child: Stack( + children: [ + getPlatformImage(peer.platform, size: isPortrait ? 38 : 30) + .paddingAll(6), + if (_shouldBuildPasswordIcon(peer)) + Positioned( + top: 1, + left: 1, + child: Icon(Icons.key, size: 6, color: Colors.white), + ), + ], + )), + Expanded( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.only( + topRight: Radius.circular(_tileRadius), + bottomRight: Radius.circular(_tileRadius), + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + children: [ + Row(children: [ + getOnline(isPortrait ? 4 : 8, peer.online), + Expanded( + child: Text( + peer.alias.isEmpty ? formatID(peer.id) : peer.alias, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall, + )), + ]).marginOnly(top: isPortrait ? 0 : 2), + Row( + children: [ + Flexible( + child: Tooltip( + message: name, + waitDuration: const Duration(seconds: 1), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + name, + style: isPortrait ? null : greyStyle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + if (showNote) + Expanded( + child: Tooltip( + message: peer.note, + waitDuration: const Duration(seconds: 1), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + peer.note, + style: isPortrait ? null : greyStyle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, + ).marginOnly( + left: peerCardUiType.value == + PeerUiType.list + ? 32 + : 4), + ), + ), + ) + ], + ), + ], + ).marginOnly(top: 2), + ), + isPortrait + ? checkBoxOrActionMorePortrait(peer) + : checkBoxOrActionMoreLandscape(peer, isTile: true), + ], + ).paddingOnly(left: 10.0, top: 3.0), + ), + ) + ], + ); + } + + Widget _buildPeerTile( + BuildContext context, Peer peer, Rx? deco) { + hideUsernameOnCard ??= + bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y'; + final colors = _frontN(peer.tags, 25) + .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) + .toList(); + return Tooltip( + message: !(isDesktop || isWebDesktop) + ? '' + : peer.tags.isNotEmpty + ? '${translate('Tags')}: ${peer.tags.join(', ')}' + : '', + child: Stack(children: [ + Obx( + () => deco == null + ? makeChild(stateGlobal.isPortrait.isTrue, peer) + : Container( + foregroundDecoration: deco.value, + child: makeChild(stateGlobal.isPortrait.isTrue, peer), + ), + ), + if (colors.isNotEmpty) + Obx(() => Positioned( + top: 2, + right: stateGlobal.isPortrait.isTrue ? 20 : 10, + child: CustomPaint( + painter: TagPainter(radius: 3, colors: colors), + ), + )) + ]), + ); + } + + Widget _buildPeerCard( + BuildContext context, Peer peer, Rx deco) { + hideUsernameOnCard ??= + bind.mainGetBuildinOption(key: kHideUsernameOnCard) == 'Y'; + final name = hideUsernameOnCard == true + ? peer.hostname + : '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; + final child = Card( + color: Colors.transparent, + elevation: 0, + margin: EdgeInsets.zero, + // to-do: memory leak here, more investigation needed. + // Continious rebuilds of `Obx()` will cause memory leak here. + // The simple demo does not have this issue. + child: Obx( + () => Container( + foregroundDecoration: deco.value, + child: ClipRRect( + borderRadius: BorderRadius.circular(_cardRadius - _borderWidth), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + color: str2color('${peer.id}${peer.platform}', 0x7f), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6), + child: + getPlatformImage(peer.platform, size: 60), + ), + Row( + children: [ + Expanded( + child: Tooltip( + message: name, + waitDuration: const Duration(seconds: 1), + child: Text( + name, + style: const TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + if (_showNote(peer)) + Row( + children: [ + Expanded( + child: Tooltip( + message: peer.note, + waitDuration: const Duration(seconds: 1), + child: Text( + peer.note, + style: const TextStyle( + color: Colors.white38, + fontSize: 10), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + )) + ], + ), + ], + ).paddingOnly(top: 4.0, left: 4.0, right: 4.0), + ), + ], + ), + ), + ), + Container( + color: Theme.of(context).colorScheme.background, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row(children: [ + getOnline(8, peer.online), + Expanded( + child: Text( + peer.alias.isEmpty ? formatID(peer.id) : peer.alias, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall, + )), + ]).paddingSymmetric(vertical: 8)), + checkBoxOrActionMoreLandscape(peer, isTile: false), + ], + ).paddingSymmetric(horizontal: 12.0), + ) + ], + ), + ), + ), + ), + ); + + final colors = _frontN(peer.tags, 25) + .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) + .toList(); + return Tooltip( + message: peer.tags.isNotEmpty + ? '${translate('Tags')}: ${peer.tags.join(', ')}' + : '', + child: Stack(children: [ + child, + if (_shouldBuildPasswordIcon(peer)) + Positioned( + top: 4, + left: 12, + child: Icon(Icons.key, size: 12, color: Colors.white), + ), + if (colors.isNotEmpty) + Positioned( + top: 4, + right: 12, + child: CustomPaint( + painter: TagPainter(radius: 4, colors: colors), + ), + ) + ]), + ); + } + + List _frontN(List list, int n) { + if (list.length <= n) { + return list; + } else { + return list.sublist(0, n); + } + } + + Widget checkBoxOrActionMorePortrait(Peer peer) { + final PeerTabModel peerTabModel = Provider.of(context); + final selected = peerTabModel.isPeerSelected(peer.id); + if (peerTabModel.multiSelectionMode) { + return Padding( + padding: const EdgeInsets.all(12), + child: selected + ? Icon( + Icons.check_box, + color: MyTheme.accent, + ) + : Icon(Icons.check_box_outline_blank), + ); + } else { + return InkWell( + child: const Padding( + padding: EdgeInsets.all(12), child: Icon(Icons.more_vert)), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(peer.id); + }); + } + } + + Widget checkBoxOrActionMoreLandscape(Peer peer, {required bool isTile}) { + final PeerTabModel peerTabModel = Provider.of(context); + final selected = peerTabModel.isPeerSelected(peer.id); + if (peerTabModel.multiSelectionMode) { + final icon = selected + ? Icon( + Icons.check_box, + color: MyTheme.accent, + ) + : Icon(Icons.check_box_outline_blank); + bool last = peerTabModel.isShiftDown && peer.id == peerTabModel.lastId; + double right = isTile ? 4 : 0; + if (last) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.accent, width: 1)), + child: icon, + ).marginOnly(right: right); + } else { + return icon.marginOnly(right: right); + } + } else { + return _actionMore(peer); + } + } + + 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(peer.id), + child: build_more(context)); + + bool _shouldBuildPasswordIcon(Peer peer) { + if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) return false; + if (gFFI.abModel.current.isPersonal()) return false; + return peer.password.isNotEmpty; + } + + /// Show the peer menu and handle user's choice. + /// User might remove the peer or send a file to the peer. + void _showPeerMenu(String id) async { + await mod_menu.showMenu( + context: context, + position: _menuPos, + items: await super.widget.popupMenuEntryBuilder(context), + elevation: 8, + ); + } + + @override + bool get wantKeepAlive => true; +} + +abstract class BasePeerCard extends StatelessWidget { + final Peer peer; + final PeerTabIndex tab; + final EdgeInsets? menuPadding; + + BasePeerCard( + {required this.peer, required this.tab, this.menuPadding, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _PeerCard( + peer: peer, + tab: tab, + connect: (BuildContext context, String id) => + connectInPeerTab(context, peer, tab), + popupMenuEntryBuilder: _buildPopupMenuEntry, + ); + } + + Future>> _buildPopupMenuEntry( + BuildContext context) async => + (await _buildMenuItems(context)) + .map((e) => e.build( + context, + const MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(); + + @protected + Future>> _buildMenuItems(BuildContext context); + + MenuEntryBase _connectCommonAction( + BuildContext context, + String title, { + bool isFileTransfer = false, + bool isViewCamera = false, + bool isTcpTunneling = false, + bool isRDP = false, + bool isTerminal = false, + bool isTerminalRunAsAdmin = false, + }) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + title, + style: style, + ), + proc: () { + if (isTerminalRunAsAdmin) { + setEnvTerminalAdmin(); + } + connectInPeerTab( + context, + peer, + tab, + isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP, + isTerminal: isTerminal || isTerminalRunAsAdmin, + ); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _connectAction(BuildContext context) { + return _connectCommonAction( + context, + (peer.alias.isEmpty + ? translate('Connect') + : '${translate('Connect')} ${peer.id}'), + ); + } + + @protected + MenuEntryBase _transferFileAction(BuildContext context) { + return _connectCommonAction( + context, + translate('Transfer file'), + isFileTransfer: true, + ); + } + + @protected + MenuEntryBase _viewCameraAction(BuildContext context) { + return _connectCommonAction( + context, + translate('View camera'), + isViewCamera: true, + ); + } + + @protected + MenuEntryBase _terminalAction(BuildContext context) { + return _connectCommonAction( + context, + '${translate('Terminal')} (beta)', + isTerminal: true, + ); + } + + @protected + MenuEntryBase _terminalRunAsAdminAction(BuildContext context) { + return _connectCommonAction( + context, + '${translate('Terminal (Run as administrator)')} (beta)', + isTerminalRunAsAdmin: true, + ); + } + + @protected + MenuEntryBase _tcpTunnelingAction(BuildContext context) { + return _connectCommonAction( + context, + translate('TCP tunneling'), + isTcpTunneling: true, + ); + } + + @protected + MenuEntryBase _rdpAction(BuildContext context, String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Container( + alignment: AlignmentDirectional.center, + height: CustomPopupMenuTheme.height, + child: Row( + children: [ + Text( + translate('RDP'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: IconButton( + icon: const Icon(Icons.edit), + padding: EdgeInsets.zero, + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + _rdpDialog(id); + }, + )), + )) + ], + )), + proc: () { + connectInPeerTab(context, peer, tab, isRDP: true); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _wolAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('WOL'), + style: style, + ), + proc: () { + bind.mainWol(id: id); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + /// Only available on Windows. + @protected + MenuEntryBase _createShortCutAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Create desktop shortcut'), + style: style, + ), + proc: () { + bind.mainCreateShortcut(id: id); + showToast(translate('Successful')); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + Future> _openNewConnInAction( + String id, String label, String key) async { + return MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate(label), + getter: () async => mainGetPeerBoolOptionSync(id, key), + setter: (bool v) async { + await bind.mainSetPeerOption( + id: id, key: key, value: bool2option(key, v)); + showToast(translate('Successful')); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + _openInTabsAction(String id) async => + await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs); + + _openInWindowsAction(String id) async => await _openNewConnInAction( + id, 'Open in new window', kOptionOpenInWindows); + + // ignore: unused_element + _openNewConnInOptAction(String id) async => + mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs) + ? await _openInWindowsAction(id) + : await _openInTabsAction(id); + + @protected + Future _isForceAlwaysRelay(String id) async { + return option2bool(kOptionForceAlwaysRelay, + (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay))); + } + + @protected + Future> _forceAlwaysRelayAction(String id) async { + return MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate('Always connect via relay'), + getter: () async { + return await _isForceAlwaysRelay(id); + }, + setter: (bool v) async { + await bind.mainSetPeerOption( + id: id, + key: kOptionForceAlwaysRelay, + value: bool2option(kOptionForceAlwaysRelay, v)); + showToast(translate('Successful')); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _renameAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Rename'), + style: style, + ), + proc: () async { + String oldName = await _getAlias(id); + renameDialog( + oldName: oldName, + onSubmit: (String newName) async { + if (newName != oldName) { + if (tab == PeerTabIndex.ab) { + await gFFI.abModel.changeAlias(id: id, alias: newName); + await bind.mainSetPeerAlias(id: id, alias: newName); + } else { + await bind.mainSetPeerAlias(id: id, alias: newName); + showToast(translate('Successful')); + _update(); + } + } + }); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _removeAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Delete'), + style: style?.copyWith(color: Colors.red), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.delete_forever, color: Colors.red), + ), + ).marginOnly(right: 4)), + ], + ), + proc: () { + onSubmit() async { + switch (tab) { + case PeerTabIndex.recent: + await bind.mainRemovePeer(id: id); + bind.mainLoadRecentPeers(); + break; + case PeerTabIndex.fav: + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + await bind.mainStoreFav(favs: favs); + bind.mainLoadFavPeers(); + } + break; + case PeerTabIndex.lan: + await bind.mainRemoveDiscovered(id: id); + bind.mainLoadLanPeers(); + break; + case PeerTabIndex.ab: + await gFFI.abModel.deletePeers([id]); + break; + case PeerTabIndex.group: + break; + } + if (tab != PeerTabIndex.ab) { + showToast(translate('Successful')); + } + } + + deleteConfirmDialog(onSubmit, + '${translate('Delete')} "${peer.alias.isEmpty ? formatID(peer.id) : peer.alias}"?'); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _unrememberPasswordAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Forget Password'), + style: style, + ), + proc: () async { + bool succ = await gFFI.abModel.changePersonalHashPassword(id, ''); + await bind.mainForgetPassword(id: id); + if (succ) { + showToast(translate('Successful')); + } else { + if (tab.index == PeerTabIndex.ab.index) { + BotToast.showText( + contentColor: Colors.red, text: translate("Failed")); + } + } + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _addFavAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Add to Favorites'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.star_outline), + ), + ).marginOnly(right: 4)), + ], + ), + proc: () { + () async { + final favs = (await bind.mainGetFav()).toList(); + if (!favs.contains(id)) { + favs.add(id); + await bind.mainStoreFav(favs: favs); + } + showToast(translate('Successful')); + }(); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _rmFavAction( + String id, Future Function() reloadFunc) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Remove from Favorites'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.star), + ), + ).marginOnly(right: 4)), + ], + ), + proc: () { + () async { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + await bind.mainStoreFav(favs: favs); + await reloadFunc(); + } + showToast(translate('Successful')); + }(); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _addToAb(Peer peer) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Add to address book'), + style: style, + ), + proc: () { + () async { + addPeersToAbDialog([Peer.copy(peer)]); + }(); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + Future _getAlias(String id) async => + await bind.mainGetPeerOption(id: id, key: 'alias'); + + @protected + void _update(); +} + +class RecentPeerCard extends BasePeerCard { + RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super( + peer: peer, + tab: PeerTabIndex.recent, + menuPadding: menuPadding, + key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context), + _transferFileAction(context), + _viewCameraAction(context), + _terminalAction(context), + ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + + final List favs = (await bind.mainGetFav()).toList(); + + if (isDesktop && peer.platform != kPeerPlatformAndroid) { + menuItems.add(_tcpTunnelingAction(context)); + } + // menuItems.add(await _openNewConnInOptAction(peer.id)); + if (!isWeb) { + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + } + if (isWindows && peer.platform == kPeerPlatformWindows) { + menuItems.add(_rdpAction(context, peer.id)); + } + if (isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + menuItems.add(MenuEntryDivider()); + if (isMobile || isDesktop || isWebDesktop) { + menuItems.add(_renameAction(peer.id)); + } + if (await bind.mainPeerHasPassword(id: peer.id)) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); + } + + if (gFFI.userModel.userName.isNotEmpty) { + menuItems.add(_addToAb(peer)); + } + + menuItems.add(MenuEntryDivider()); + menuItems.add(_removeAction(peer.id)); + return menuItems; + } + + @protected + @override + void _update() => bind.mainLoadRecentPeers(); +} + +class FavoritePeerCard extends BasePeerCard { + FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super( + peer: peer, + tab: PeerTabIndex.fav, + menuPadding: menuPadding, + key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context), + _transferFileAction(context), + _viewCameraAction(context), + _terminalAction(context), + ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + + if (isDesktop && peer.platform != kPeerPlatformAndroid) { + menuItems.add(_tcpTunnelingAction(context)); + } + // menuItems.add(await _openNewConnInOptAction(peer.id)); + if (!isWeb) { + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + } + if (isWindows && peer.platform == kPeerPlatformWindows) { + menuItems.add(_rdpAction(context, peer.id)); + } + if (isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + menuItems.add(MenuEntryDivider()); + if (isMobile || isDesktop || isWebDesktop) { + menuItems.add(_renameAction(peer.id)); + } + if (await bind.mainPeerHasPassword(id: peer.id)) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + menuItems.add(_rmFavAction(peer.id, () async { + await bind.mainLoadFavPeers(); + })); + + if (gFFI.userModel.userName.isNotEmpty) { + menuItems.add(_addToAb(peer)); + } + + menuItems.add(MenuEntryDivider()); + menuItems.add(_removeAction(peer.id)); + return menuItems; + } + + @protected + @override + void _update() => bind.mainLoadFavPeers(); +} + +class DiscoveredPeerCard extends BasePeerCard { + DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super( + peer: peer, + tab: PeerTabIndex.lan, + menuPadding: menuPadding, + key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context), + _transferFileAction(context), + _viewCameraAction(context), + _terminalAction(context), + ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + + final List favs = (await bind.mainGetFav()).toList(); + + if (isDesktop && peer.platform != kPeerPlatformAndroid) { + menuItems.add(_tcpTunnelingAction(context)); + } + // menuItems.add(await _openNewConnInOptAction(peer.id)); + if (!isWeb) { + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + } + if (isWindows && peer.platform == kPeerPlatformWindows) { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(_wolAction(peer.id)); + if (isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); + } + + if (gFFI.userModel.userName.isNotEmpty) { + menuItems.add(_addToAb(peer)); + } + + menuItems.add(MenuEntryDivider()); + menuItems.add(_removeAction(peer.id)); + return menuItems; + } + + @protected + @override + void _update() => bind.mainLoadLanPeers(); +} + +class AddressBookPeerCard extends BasePeerCard { + AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super( + peer: peer, + tab: PeerTabIndex.ab, + menuPadding: menuPadding, + key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context), + _transferFileAction(context), + _viewCameraAction(context), + _terminalAction(context), + ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + + if (isDesktop && peer.platform != kPeerPlatformAndroid) { + menuItems.add(_tcpTunnelingAction(context)); + } + // menuItems.add(await _openNewConnInOptAction(peer.id)); + if (!isWeb) { + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + } + if (isWindows && peer.platform == kPeerPlatformWindows) { + menuItems.add(_rdpAction(context, peer.id)); + } + if (isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + if (gFFI.abModel.current.canWrite()) { + menuItems.add(MenuEntryDivider()); + if (isMobile || isDesktop || isWebDesktop) { + menuItems.add(_renameAction(peer.id)); + } + if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + if (!gFFI.abModel.current.isPersonal()) { + menuItems.add(_changeSharedAbPassword()); + } + if (gFFI.abModel.currentAbTags.isNotEmpty) { + menuItems.add(_editTagAction(peer.id)); + } + menuItems.add(_editNoteAction(peer.id)); + } + final addressbooks = gFFI.abModel.addressBooksCanWrite(); + if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) { + addressbooks.remove(gFFI.abModel.currentName.value); + } + if (addressbooks.isNotEmpty) { + menuItems.add(_addToAb(peer)); + } + menuItems.add(_existIn()); + if (gFFI.abModel.current.canWrite()) { + menuItems.add(MenuEntryDivider()); + menuItems.add(_removeAction(peer.id)); + } + return menuItems; + } + + // address book does not need to update + @protected + @override + void _update() => + {}; //gFFI.abModel.pullAb(force: ForcePullAb.current, quiet: true); + + @protected + MenuEntryBase _editTagAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Edit Tag'), + style: style, + ), + proc: () { + editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async { + await gFFI.abModel.changeTagForPeers([id], selectedTag); + }); + }, + padding: super.menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _editNoteAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Edit note'), + style: style, + ), + proc: () { + editAbPeerNoteDialog(id); + }, + padding: super.menuPadding, + dismissOnClicked: true, + ); + } + + @protected + @override + Future _getAlias(String id) async => + gFFI.abModel.find(id)?.alias ?? ''; + + MenuEntryBase _changeSharedAbPassword() { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate( + peer.password.isEmpty ? 'Set shared password' : 'Change Password'), + style: style, + ), + proc: () { + setSharedAbPasswordDialog(gFFI.abModel.currentName.value, peer); + }, + padding: super.menuPadding, + dismissOnClicked: true, + ); + } + + MenuEntryBase _existIn() { + final names = gFFI.abModel.idExistIn(peer.id); + final text = names.join(', '); + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Exist in'), + style: style, + ), + proc: () { + gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + title: Text(translate('Exist in')), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text(text)]), + actions: [ + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: close, + ), + ], + onSubmit: close, + onCancel: close, + ); + }); + }, + padding: super.menuPadding, + dismissOnClicked: true, + ); + } +} + +class MyGroupPeerCard extends BasePeerCard { + MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super( + peer: peer, + tab: PeerTabIndex.group, + menuPadding: menuPadding, + key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context), + _transferFileAction(context), + _viewCameraAction(context), + _terminalAction(context), + ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + + if (isDesktop && peer.platform != kPeerPlatformAndroid) { + menuItems.add(_tcpTunnelingAction(context)); + } + // menuItems.add(await _openNewConnInOptAction(peer.id)); + if (!isWeb) { + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + } + if (isWindows && peer.platform == kPeerPlatformWindows) { + menuItems.add(_rdpAction(context, peer.id)); + } + if (isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + // menuItems.add(MenuEntryDivider()); + // menuItems.add(_renameAction(peer.id)); + // if (await bind.mainPeerHasPassword(id: peer.id)) { + // menuItems.add(_unrememberPasswordAction(peer.id)); + // } + if (gFFI.userModel.userName.isNotEmpty) { + menuItems.add(_addToAb(peer)); + } + return menuItems; + } + + @protected + @override + void _update() => gFFI.groupModel.pull(); +} + +void _rdpDialog(String id) async { + final maxLength = bind.mainMaxEncryptLen(); + final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); + final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); + final portController = TextEditingController(text: port); + final userController = TextEditingController(text: username); + final passwordController = TextEditingController( + text: await bind.mainGetPeerOption(id: id, key: 'rdp_password')); + RxBool secure = true.obs; + + gFFI.dialogManager.show((setState, close, context) { + submit() async { + String port = portController.text.trim(); + String username = userController.text; + String password = passwordController.text; + await bind.mainSetPeerOption(id: id, key: 'rdp_port', value: port); + await bind.mainSetPeerOption( + id: id, key: 'rdp_username', value: username); + await bind.mainSetPeerOption( + id: id, key: 'rdp_password', value: password); + showToast(translate('Successful')); + close(); + } + + return CustomAlertDialog( + title: Text(translate('RDP Settings')), + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + isDesktop + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Port')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: TextField( + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')) + ], + decoration: InputDecoration( + labelText: isDesktop ? null : translate('Port'), + hintText: '3389'), + controller: portController, + autofocus: true, + ).workaroundFreezeLinuxMint(), + ), + ], + ).marginOnly(bottom: isDesktop ? 8 : 0), + Obx(() => Row( + children: [ + stateGlobal.isPortrait.isFalse + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Username')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: + isDesktop ? null : translate('Username')), + controller: userController, + ).workaroundFreezeLinuxMint(), + ), + ], + ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)), + Obx(() => Row( + children: [ + stateGlobal.isPortrait.isFalse + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Password')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), + Expanded( + child: Obx(() => TextField( + obscureText: secure.value, + maxLength: maxLength, + decoration: InputDecoration( + labelText: + isDesktop ? null : translate('Password'), + suffixIcon: IconButton( + onPressed: () => + secure.value = !secure.value, + icon: Icon(secure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: passwordController, + ).workaroundFreezeLinuxMint()), + ), + ], + )) + ], + ), + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +Widget getOnline(double rightPadding, bool online) { + return Tooltip( + message: translate(online ? 'Online' : 'Offline'), + waitDuration: const Duration(seconds: 1), + child: Padding( + padding: EdgeInsets.fromLTRB(0, 4, rightPadding, 4), + child: CircleAvatar( + radius: 3, backgroundColor: online ? Colors.green : kColorWarn))); +} + +Widget build_more(BuildContext context, {bool invert = false}) { + final RxBool hover = false.obs; + return InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () {}, + onHover: (value) => hover.value = value, + child: Obx(() => CircleAvatar( + radius: 14, + backgroundColor: hover.value + ? (invert + ? Theme.of(context).colorScheme.background + : Theme.of(context).scaffoldBackgroundColor) + : (invert + ? Theme.of(context).scaffoldBackgroundColor + : Theme.of(context).colorScheme.background), + child: Icon(Icons.more_vert, + size: 18, + color: hover.value + ? Theme.of(context).textTheme.titleLarge?.color + : Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5))))); +} + +class TagPainter extends CustomPainter { + final double radius; + late final List colors; + + TagPainter({required this.radius, required List colors}) { + this.colors = colors.reversed.toList(); + } + + @override + void paint(Canvas canvas, Size size) { + double x = 0; + double y = radius; + for (int i = 0; i < colors.length; i++) { + Paint paint = Paint(); + paint.color = colors[i]; + x -= radius + 1; + if (i == colors.length - 1) { + canvas.drawCircle(Offset(x, y), radius, paint); + } else { + Path path = Path(); + path.addArc(Rect.fromCircle(center: Offset(x, y), radius: radius), + math.pi * 4 / 3, math.pi * 4 / 3); + path.addArc( + Rect.fromCircle(center: Offset(x - radius, y), radius: radius), + math.pi * 5 / 3, + math.pi * 2 / 3); + path.fillType = PathFillType.evenOdd; + canvas.drawPath(path, paint); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} + +void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, + {bool isFileTransfer = false, + bool isViewCamera = false, + bool isTcpTunneling = false, + bool isRDP = false, + bool isTerminal = false}) async { + var password = ''; + bool isSharedPassword = false; + if (tab == PeerTabIndex.ab) { + // If recent peer's alias is empty, set it to ab's alias + // Because the platform is not set, it may not take effect, but it is more important not to display if the connection is not successful + if (peer.alias.isNotEmpty && + (await bind.mainGetPeerOption(id: peer.id, key: "alias")).isEmpty) { + await bind.mainSetPeerAlias( + id: peer.id, + alias: peer.alias, + ); + } + if (!gFFI.abModel.current.isPersonal()) { + if (peer.password.isNotEmpty) { + password = peer.password; + isSharedPassword = true; + } + if (password.isEmpty) { + final abPassword = gFFI.abModel.getdefaultSharedPassword(); + if (abPassword != null) { + password = abPassword; + isSharedPassword = true; + } + } + } + } + connect(context, peer.id, + password: password, + isSharedPassword: isSharedPassword, + isFileTransfer: isFileTransfer, + isTerminal: isTerminal, + isViewCamera: isViewCamera, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/peer_tab_page.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/peer_tab_page.dart new file mode 100644 index 0000000..4849f27 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/peer_tab_page.dart @@ -0,0 +1,1039 @@ +import 'dart:ui' as ui; + +import 'package:bot_toast/bot_toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/address_book.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; +import 'package:flutter_hbb/common/widgets/my_group.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:flutter_hbb/common/widgets/peer_card.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/ab_model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; + +import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_down_button/pull_down_button.dart'; + +import '../../common.dart'; +import '../../models/platform_model.dart'; + +class PeerTabPage extends StatefulWidget { + const PeerTabPage({Key? key}) : super(key: key); + @override + State createState() => _PeerTabPageState(); +} + +class _TabEntry { + final Widget widget; + final Function({dynamic hint})? load; + _TabEntry(this.widget, [this.load]); +} + +EdgeInsets? _menuPadding() { + return (isDesktop || isWebDesktop) ? kDesktopMenuPadding : null; +} + +class _PeerTabPageState extends State + with SingleTickerProviderStateMixin { + final List<_TabEntry> entries = [ + _TabEntry(RecentPeersView( + menuPadding: _menuPadding(), + )), + _TabEntry(FavoritePeersView( + menuPadding: _menuPadding(), + )), + _TabEntry(DiscoveredPeersView( + menuPadding: _menuPadding(), + )), + _TabEntry( + AddressBook( + menuPadding: _menuPadding(), + ), + ({dynamic hint}) => gFFI.abModel.pullAb( + force: hint == null ? ForcePullAb.listAndCurrent : null, + quiet: false)), + _TabEntry( + MyGroup( + menuPadding: _menuPadding(), + ), + ({dynamic hint}) => gFFI.groupModel.pull(force: hint == null), + ), + ]; + RelativeRect? mobileTabContextMenuPos; + + final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible); + + _PeerTabPageState() { + _loadLocalOptions(); + } + + void _loadLocalOptions() { + final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType); + if (uiType != '') { + peerCardUiType.value = int.parse(uiType) == 0 + ? PeerUiType.grid + : int.parse(uiType) == 1 + ? PeerUiType.tile + : PeerUiType.list; + } + hideAbTagsPanel.value = + bind.mainGetLocalOption(key: kOptionHideAbTagsPanel) == 'Y'; + } + + Future handleTabSelection(int tabIndex) async { + if (tabIndex < entries.length) { + if (tabIndex != gFFI.peerTabModel.currentTab) { + gFFI.peerTabModel.setCurrentTabCachedPeers([]); + } + gFFI.peerTabModel.setCurrentTab(tabIndex); + entries[tabIndex].load?.call(hint: false); + } + } + + @override + Widget build(BuildContext context) { + final model = Provider.of(context); + Widget selectionWrap(Widget widget) { + return model.multiSelectionMode ? createMultiSelectionBar(model) : widget; + } + + return Column( + textBaseline: TextBaseline.ideographic, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() => SizedBox( + height: 32, + child: Container( + padding: stateGlobal.isPortrait.isTrue + ? EdgeInsets.symmetric(horizontal: 2) + : null, + child: selectionWrap(Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: visibleContextMenuListener( + _createSwitchBar(context))), + if (stateGlobal.isPortrait.isTrue) + ..._portraitRightActions(context) + else + ..._landscapeRightActions(context) + ], + )), + ), + ).paddingOnly(right: stateGlobal.isPortrait.isTrue ? 0 : 12)), + _createPeersView(), + ], + ); + } + + Widget _createSwitchBar(BuildContext context) { + final model = Provider.of(context); + var counter = -1; + return ReorderableListView( + buildDefaultDragHandles: false, + onReorder: model.reorder, + scrollDirection: Axis.horizontal, + physics: NeverScrollableScrollPhysics(), + children: model.visibleEnabledOrderedIndexs.map((t) { + final selected = model.currentTab == t; + final color = selected + ? MyTheme.tabbar(context).selectedTextColor + : MyTheme.tabbar(context).unSelectedTextColor + ?..withOpacity(0.5); + final hover = false.obs; + final deco = BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(6)); + final decoBorder = BoxDecoration( + border: Border( + bottom: BorderSide(width: 2, color: color!), + )); + counter += 1; + return ReorderableDragStartListener( + key: ValueKey(t), + index: counter, + child: Obx(() => Tooltip( + preferBelow: false, + message: model.tabTooltip(t), + onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, + child: InkWell( + child: Container( + decoration: (hover.value + ? (selected ? decoBorder : deco) + : (selected ? decoBorder : null)), + child: Icon(model.tabIcon(t), color: color) + .paddingSymmetric(horizontal: 4), + ).paddingSymmetric(horizontal: 4), + onTap: isOptionFixed(kOptionPeerTabIndex) + ? null + : () async { + await handleTabSelection(t); + await bind.setLocalFlutterOption( + k: kOptionPeerTabIndex, v: t.toString()); + }, + onHover: (value) => hover.value = value, + ), + ))); + }).toList()); + } + + Widget _createPeersView() { + final model = Provider.of(context); + Widget child; + if (model.visibleEnabledOrderedIndexs.isEmpty) { + child = visibleContextMenuListener(Row( + children: [Expanded(child: InkWell())], + )); + } else { + if (model.visibleEnabledOrderedIndexs.contains(model.currentTab)) { + child = entries[model.currentTab].widget; + } else { + debugPrint("should not happen! currentTab not in visibleIndexs"); + Future.delayed(Duration.zero, () { + model.setCurrentTab(model.visibleEnabledOrderedIndexs[0]); + }); + child = entries[0].widget; + } + } + return Expanded( + child: child.marginSymmetric( + vertical: (isDesktop || isWebDesktop) ? 12.0 : 6.0)); + } + + Widget _createRefresh( + {required PeerTabIndex index, required RxBool loading}) { + final model = Provider.of(context); + final textColor = Theme.of(context).textTheme.titleLarge?.color; + return Offstage( + offstage: model.currentTab != index.index, + child: Tooltip( + message: translate('Refresh'), + child: RefreshWidget( + onPressed: () { + if (gFFI.peerTabModel.currentTab < entries.length) { + entries[gFFI.peerTabModel.currentTab].load?.call(); + } + }, + spinning: loading, + child: RotatedBox( + quarterTurns: 2, + child: Icon( + Icons.refresh, + size: 18, + color: textColor, + ))), + ), + ); + } + + Widget _createPeerViewTypeSwitch(BuildContext context) { + return PeerViewDropdown(); + } + + Widget _createMultiSelection() { + final textColor = Theme.of(context).textTheme.titleLarge?.color; + final model = Provider.of(context); + return _hoverAction( + toolTip: translate('Select'), + context: context, + onTap: () { + model.setMultiSelectionMode(true); + if (isMobile && Navigator.canPop(context)) { + Navigator.pop(context); + } + }, + child: SvgPicture.asset( + "assets/checkbox-outline.svg", + width: 18, + height: 18, + colorFilter: svgColor(textColor), + ), + ); + } + + void mobileShowTabVisibilityMenu() { + final model = gFFI.peerTabModel; + final items = List.empty(growable: true); + for (int i = 0; i < PeerTabModel.maxTabCount; i++) { + if (!model.isEnabled[i]) continue; + items.add(PopupMenuItem( + height: kMinInteractiveDimension * 0.8, + onTap: isOptVisiableFixed + ? null + : () => model.setTabVisible(i, !model.isVisibleEnabled[i]), + enabled: !isOptVisiableFixed, + child: Row( + children: [ + Checkbox( + value: model.isVisibleEnabled[i], + onChanged: isOptVisiableFixed + ? null + : (_) { + model.setTabVisible(i, !model.isVisibleEnabled[i]); + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }), + Expanded(child: Text(model.tabTooltip(i))), + ], + ), + )); + } + if (mobileTabContextMenuPos != null) { + showMenu( + context: context, position: mobileTabContextMenuPos!, items: items); + } + } + + Widget visibleContextMenuListener(Widget child) { + if (!(isDesktop || isWebDesktop)) { + return GestureDetector( + onLongPressDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + mobileTabContextMenuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onLongPressUp: () { + mobileShowTabVisibilityMenu(); + }, + child: child, + ); + } else { + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + if (e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return visibleContextMenu(cancelFunc); + }, + target: e.position, + ); + } + }, + child: child); + } + } + + Widget visibleContextMenu(CancelFunc cancelFunc) { + final model = Provider.of(context); + final menu = List.empty(growable: true); + for (int i = 0; i < model.orders.length; i++) { + int tabIndex = model.orders[i]; + if (tabIndex < 0 || tabIndex >= PeerTabModel.maxTabCount) continue; + if (!model.isEnabled[tabIndex]) continue; + menu.add(MenuEntrySwitchSync( + switchType: SwitchType.scheckbox, + text: model.tabTooltip(tabIndex), + currentValue: model.isVisibleEnabled[tabIndex], + setter: (show) async { + model.setTabVisible(tabIndex, show); + // Do not hide the current menu (checkbox) + // cancelFunc(); + }, + enabled: (!isOptVisiableFixed).obs)); + } + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: MyTheme.accent, + height: 20.0, + dividerHeight: 12.0, + ))) + .expand((i) => i) + .toList()); + } + + Widget createMultiSelectionBar(PeerTabModel model) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Offstage( + offstage: model.selectedPeers.isEmpty, + child: Row( + children: [ + deleteSelection(), + addSelectionToFav(), + addSelectionToAb(), + editSelectionTags(), + ], + ), + ), + Row( + children: [ + selectionCount(model.selectedPeers.length), + selectAll(model), + closeSelection(), + ], + ) + ], + ); + } + + Widget deleteSelection() { + final model = Provider.of(context); + if (model.currentTab == PeerTabIndex.group.index) { + return Offstage(); + } + return _hoverAction( + context: context, + toolTip: translate('Delete'), + onTap: () { + onSubmit() async { + final peers = model.selectedPeers; + switch (model.currentTab) { + case 0: + for (var p in peers) { + await bind.mainRemovePeer(id: p.id); + } + bind.mainLoadRecentPeers(); + break; + case 1: + final favs = (await bind.mainGetFav()).toList(); + peers.map((p) { + favs.remove(p.id); + }).toList(); + await bind.mainStoreFav(favs: favs); + bind.mainLoadFavPeers(); + break; + case 2: + for (var p in peers) { + await bind.mainRemoveDiscovered(id: p.id); + } + bind.mainLoadLanPeers(); + break; + case 3: + await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList()); + break; + default: + break; + } + gFFI.peerTabModel.setMultiSelectionMode(false); + if (model.currentTab != 3) showToast(translate('Successful')); + } + + deleteConfirmDialog(onSubmit, translate('Delete')); + }, + child: Icon(Icons.delete, color: Colors.red)); + } + + Widget addSelectionToFav() { + final model = Provider.of(context); + return Offstage( + offstage: + model.currentTab != PeerTabIndex.recent.index, // show based on recent + child: _hoverAction( + context: context, + toolTip: translate('Add to Favorites'), + onTap: () async { + final peers = model.selectedPeers; + final favs = (await bind.mainGetFav()).toList(); + for (var p in peers) { + if (!favs.contains(p.id)) { + favs.add(p.id); + } + } + await bind.mainStoreFav(favs: favs); + model.setMultiSelectionMode(false); + showToast(translate('Successful')); + }, + child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]), + ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), + ); + } + + Widget addSelectionToAb() { + final model = Provider.of(context); + final addressbooks = gFFI.abModel.addressBooksCanWrite(); + if (model.currentTab == PeerTabIndex.ab.index) { + addressbooks.remove(gFFI.abModel.currentName.value); + } + return Offstage( + offstage: !gFFI.userModel.isLogin || addressbooks.isEmpty, + child: _hoverAction( + context: context, + toolTip: translate('Add to address book'), + onTap: () { + final peers = model.selectedPeers.map((e) => Peer.copy(e)).toList(); + addPeersToAbDialog(peers); + model.setMultiSelectionMode(false); + }, + child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]), + ).marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), + ); + } + + Widget editSelectionTags() { + final model = Provider.of(context); + return Offstage( + offstage: !gFFI.userModel.isLogin || + model.currentTab != PeerTabIndex.ab.index || + gFFI.abModel.currentAbTags.isEmpty, + child: _hoverAction( + context: context, + toolTip: translate('Edit Tag'), + onTap: () { + editAbTagDialog(List.empty(), (selectedTags) async { + final peers = model.selectedPeers; + await gFFI.abModel.changeTagForPeers( + peers.map((p) => p.id).toList(), selectedTags); + model.setMultiSelectionMode(false); + showToast(translate('Successful')); + }); + }, + child: Icon(Icons.tag)) + .marginOnly(left: !(isDesktop || isWebDesktop) ? 11 : 6), + ); + } + + Widget selectionCount(int count) { + return Align( + alignment: Alignment.center, + child: Text('$count ${translate('Selected')}'), + ); + } + + Widget selectAll(PeerTabModel model) { + return Offstage( + offstage: + model.selectedPeers.length >= model.currentTabCachedPeers.length, + child: _hoverAction( + context: context, + toolTip: translate('Select All'), + onTap: () { + model.selectAll(); + }, + child: Icon(Icons.select_all), + ).marginOnly(left: 6), + ); + } + + Widget closeSelection() { + final model = Provider.of(context); + return _hoverAction( + context: context, + toolTip: translate('Close'), + onTap: () { + model.setMultiSelectionMode(false); + }, + child: Icon(Icons.clear)) + .marginOnly(left: 6); + } + + Widget _toggleTags() { + return _hoverAction( + context: context, + toolTip: translate('Toggle Tags'), + hoverableWhenfalse: hideAbTagsPanel, + child: Icon( + Icons.tag_rounded, + size: 18, + ), + onTap: () async { + await bind.mainSetLocalOption( + key: kOptionHideAbTagsPanel, + value: hideAbTagsPanel.value ? defaultOptionNo : "Y"); + hideAbTagsPanel.value = !hideAbTagsPanel.value; + }); + } + + List _landscapeRightActions(BuildContext context) { + final model = Provider.of(context); + return [ + const PeerSearchBar().marginOnly(right: 13), + _createRefresh( + index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), + _createRefresh( + index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading), + Offstage( + offstage: model.currentTabCachedPeers.isEmpty, + child: _createMultiSelection(), + ), + _createPeerViewTypeSwitch(context), + Offstage( + offstage: model.currentTab == PeerTabIndex.recent.index, + child: PeerSortDropdown(), + ), + Offstage( + offstage: model.currentTab != PeerTabIndex.ab.index, + child: _toggleTags(), + ), + ]; + } + + List _portraitRightActions(BuildContext context) { + final model = Provider.of(context); + final screenWidth = MediaQuery.of(context).size.width; + final leftIconSize = Theme.of(context).iconTheme.size ?? 24; + final leftActionsSize = + (leftIconSize + (4 + 4) * 2) * model.visibleEnabledOrderedIndexs.length; + final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2; + final searchWidth = 120; + final otherActionWidth = 18 + 10; + + dropDown(List menus) { + final padding = 6.0; + final textColor = Theme.of(context).textTheme.titleLarge?.color; + return PullDownButton( + buttonBuilder: + (BuildContext context, Future Function() showMenu) { + return _hoverAction( + context: context, + toolTip: translate('More'), + child: SvgPicture.asset( + "assets/chevron_up_chevron_down.svg", + width: 18, + height: 18, + colorFilter: svgColor(textColor), + ), + onTap: showMenu, + ); + }, + routeTheme: PullDownMenuRouteTheme( + width: menus.length * (otherActionWidth + padding * 2) * 1.0), + itemBuilder: (context) => [ + PullDownMenuEntryImpl( + child: Row( + mainAxisSize: MainAxisSize.min, + children: menus + .map((e) => + Material(child: e.paddingSymmetric(horizontal: padding))) + .toList(), + ), + ) + ], + ); + } + + // Always show search, refresh + List actions = [ + const PeerSearchBar(), + if (model.currentTab == PeerTabIndex.ab.index) + _createRefresh( + index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), + if (model.currentTab == PeerTabIndex.group.index) + _createRefresh( + index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading), + ]; + final List dynamicActions = [ + if (model.currentTabCachedPeers.isNotEmpty) _createMultiSelection(), + if (model.currentTab != PeerTabIndex.recent.index) PeerSortDropdown(), + if (model.currentTab == PeerTabIndex.ab.index) _toggleTags() + ]; + final rightWidth = availableWidth - + searchWidth - + (actions.length == 2 ? otherActionWidth : 0); + final availablePositions = rightWidth ~/ otherActionWidth; + + if (availablePositions < dynamicActions.length && + dynamicActions.length > 1) { + if (availablePositions < 2) { + actions.addAll([ + dropDown(dynamicActions), + ]); + } else { + actions.addAll([ + ...dynamicActions.sublist(0, availablePositions - 1), + dropDown(dynamicActions.sublist(availablePositions - 1)), + ]); + } + } else { + actions.addAll(dynamicActions); + } + return actions; + } +} + +class PeerSearchBar extends StatefulWidget { + const PeerSearchBar({Key? key}) : super(key: key); + + @override + State createState() => _PeerSearchBarState(); +} + +class _PeerSearchBarState extends State { + var drawer = false; + + @override + Widget build(BuildContext context) { + return drawer + ? _buildSearchBar() + : _hoverAction( + context: context, + toolTip: translate('Search'), + padding: const EdgeInsets.only(right: 2), + onTap: () { + setState(() { + drawer = true; + }); + }, + child: Icon( + Icons.search_rounded, + color: Theme.of(context).hintColor, + )); + } + + Widget _buildSearchBar() { + RxBool focused = false.obs; + FocusNode focusNode = FocusNode(); + focusNode.addListener(() { + focused.value = focusNode.hasFocus; + peerSearchTextController.selection = TextSelection( + baseOffset: 0, + extentOffset: peerSearchTextController.value.text.length); + }); + return Obx(() => Container( + width: stateGlobal.isPortrait.isTrue ? 120 : 140, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + Icon( + Icons.search_rounded, + color: Theme.of(context).hintColor, + ).marginSymmetric(horizontal: 4), + Expanded( + child: TextField( + autofocus: true, + controller: peerSearchTextController, + onChanged: (searchText) { + peerSearchText.value = searchText; + }, + focusNode: focusNode, + textAlign: TextAlign.start, + maxLines: 1, + cursorColor: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5), + cursorHeight: 18, + cursorWidth: 1, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(vertical: 6), + hintText: + focused.value ? null : translate("Search ID"), + hintStyle: TextStyle( + fontSize: 14, color: Theme.of(context).hintColor), + border: InputBorder.none, + isDense: true, + ), + ).workaroundFreezeLinuxMint(), + ), + // Icon(Icons.close), + IconButton( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 2), + onPressed: () { + setState(() { + peerSearchTextController.clear(); + peerSearchText.value = ""; + drawer = false; + }); + }, + icon: Tooltip( + message: translate('Close'), + child: Icon( + Icons.close, + color: Theme.of(context).hintColor, + )), + ), + ], + ), + ) + ], + ), + )); + } +} + +class PeerViewDropdown extends StatefulWidget { + const PeerViewDropdown({super.key}); + + @override + State createState() => _PeerViewDropdownState(); +} + +class _PeerViewDropdownState extends State { + @override + Widget build(BuildContext context) { + final List types = [ + PeerUiType.grid, + PeerUiType.tile, + PeerUiType.list + ]; + final style = TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + List items = List.empty(growable: true); + items.add(PopupMenuItem( + height: 36, + enabled: false, + child: Text(translate("Change view"), style: style))); + for (var e in PeerUiType.values) { + items.add(PopupMenuItem( + height: 36, + child: Obx(() => Center( + child: SizedBox( + height: 36, + child: getRadio( + Tooltip( + message: translate(types.indexOf(e) == 0 + ? 'Big tiles' + : types.indexOf(e) == 1 + ? 'Small tiles' + : 'List'), + child: Icon( + e == PeerUiType.grid + ? Icons.grid_view_rounded + : e == PeerUiType.list + ? Icons.view_list_rounded + : Icons.view_agenda_rounded, + size: 18, + )), + e, + peerCardUiType.value, + dense: true, + isOptionFixed(kOptionPeerCardUiType) + ? null + : (PeerUiType? v) async { + if (v != null) { + peerCardUiType.value = v; + setState(() {}); + await bind.setLocalFlutterOption( + k: kOptionPeerCardUiType, + v: peerCardUiType.value.index.toString(), + ); + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + } + }), + ), + )))); + } + + var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0); + return _hoverAction( + context: context, + toolTip: translate('Change view'), + child: Icon( + peerCardUiType.value == PeerUiType.grid + ? Icons.grid_view_rounded + : peerCardUiType.value == PeerUiType.list + ? Icons.view_list_rounded + : Icons.view_agenda_rounded, + size: 18, + ), + onTapDown: (details) { + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () => showMenu( + context: context, + position: menuPos, + items: items, + elevation: 8, + )); + } +} + +class PeerSortDropdown extends StatefulWidget { + const PeerSortDropdown({super.key}); + + @override + State createState() => _PeerSortDropdownState(); +} + +class _PeerSortDropdownState extends State { + _PeerSortDropdownState() { + if (!PeerSortType.values.contains(peerSort.value)) { + _loadLocalOptions(); + } + } + + void _loadLocalOptions() { + peerSort.value = PeerSortType.remoteId; + bind.setLocalFlutterOption( + k: kOptionPeerSorting, + v: peerSort.value, + ); + } + + @override + Widget build(BuildContext context) { + final style = TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + List items = List.empty(growable: true); + items.add(PopupMenuItem( + height: 36, + enabled: false, + child: Text(translate("Sort by"), style: style))); + for (var e in PeerSortType.values) { + items.add(PopupMenuItem( + height: 36, + child: Obx(() => Center( + child: SizedBox( + height: 36, + child: getRadio( + Text(translate(e), style: style), e, peerSort.value, + dense: true, (String? v) async { + if (v != null) { + peerSort.value = v; + await bind.setLocalFlutterOption( + k: kOptionPeerSorting, + v: peerSort.value, + ); + } + }), + ), + )))); + } + + var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0); + return _hoverAction( + context: context, + toolTip: translate('Sort by'), + child: Icon( + Icons.sort_rounded, + size: 18, + ), + onTapDown: (details) { + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () => showMenu( + context: context, + position: menuPos, + items: items, + elevation: 8, + ), + ); + } +} + +class RefreshWidget extends StatefulWidget { + final VoidCallback onPressed; + final Widget child; + final RxBool? spinning; + const RefreshWidget( + {super.key, required this.onPressed, required this.child, this.spinning}); + + @override + State createState() => RefreshWidgetState(); +} + +class RefreshWidgetState extends State { + double turns = 0.0; + bool hover = false; + + @override + void initState() { + super.initState(); + widget.spinning?.listen((v) { + if (v && mounted) { + setState(() { + turns += 1; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + final deco = BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(6), + ); + return AnimatedRotation( + turns: turns, + duration: const Duration(milliseconds: 200), + onEnd: () { + if (widget.spinning?.value == true && mounted) { + setState(() => turns += 1.0); + } + }, + child: Container( + padding: EdgeInsets.all(4.0), + margin: EdgeInsets.symmetric(horizontal: 1), + decoration: hover ? deco : null, + child: InkWell( + onTap: () { + if (mounted) setState(() => turns += 1.0); + widget.onPressed(); + }, + onHover: (value) { + if (mounted) { + setState(() { + hover = value; + }); + } + }, + child: widget.child), + )); + } +} + +Widget _hoverAction( + {required BuildContext context, + required Widget child, + required Function() onTap, + required String toolTip, + GestureTapDownCallback? onTapDown, + RxBool? hoverableWhenfalse, + EdgeInsetsGeometry padding = const EdgeInsets.all(4.0)}) { + final hover = false.obs; + final deco = BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(6), + ); + return Tooltip( + message: toolTip, + child: Obx( + () => Container( + margin: EdgeInsets.symmetric(horizontal: 1), + decoration: + (hover.value || hoverableWhenfalse?.value == false) ? deco : null, + child: InkWell( + onHover: (value) => hover.value = value, + onTap: onTap, + onTapDown: onTapDown, + child: Container(padding: padding, child: child))), + ), + ); +} + +class PullDownMenuEntryImpl extends StatelessWidget + implements PullDownMenuEntry { + final Widget child; + const PullDownMenuEntryImpl({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return child; + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/peers_view.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/peers_view.dart new file mode 100644 index 0000000..5be5af2 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/peers_view.dart @@ -0,0 +1,598 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:dynamic_layouts/dynamic_layouts.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/ab_model.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/state_model.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 'peer_card.dart'; + +typedef PeerFilter = bool Function(Peer peer); +typedef PeerCardBuilder = Widget Function(Peer peer); + +class PeerSortType { + static const String remoteId = 'Remote ID'; + static const String remoteHost = 'Remote Host'; + static const String username = 'Username'; + static const String status = 'Status'; + + static List values = [ + PeerSortType.remoteId, + PeerSortType.remoteHost, + PeerSortType.username, + PeerSortType.status + ]; +} + +class LoadEvent { + static const String recent = 'load_recent_peers'; + static const String favorite = 'load_fav_peers'; + static const String lan = 'load_lan_peers'; + static const String addressBook = 'load_address_book_peers'; + static const String group = 'load_group_peers'; +} + +class PeersModelName { + static const String recent = 'recent peer'; + static const String favorite = 'fav peer'; + static const String lan = 'discovered peer'; + static const String addressBook = 'address book peer'; + static const String group = 'group peer'; +} + +/// for peer search text, global obs value +final peerSearchText = "".obs; + +/// for peer sort, global obs value +RxString? _peerSort; +RxString get peerSort { + _peerSort ??= bind.getLocalFlutterOption(k: kOptionPeerSorting).obs; + return _peerSort!; +} + +// list for listener +RxList get obslist => [peerSearchText, peerSort].obs; + +final peerSearchTextController = + TextEditingController(text: peerSearchText.value); + +class _PeersView extends StatefulWidget { + final Peers peers; + final PeerFilter? peerFilter; + final PeerCardBuilder peerCardBuilder; + final PeerTabIndex peerTabIndex; + + const _PeersView( + {required this.peers, + required this.peerCardBuilder, + required this.peerTabIndex, + this.peerFilter, + Key? key}) + : super(key: key); + + @override + _PeersViewState createState() => _PeersViewState(); +} + +/// State for the peer widget. +class _PeersViewState extends State<_PeersView> + with WindowListener, WidgetsBindingObserver { + static const int _maxQueryCount = 3; + final HashMap _emptyMessages = HashMap.from({ + LoadEvent.recent: 'empty_recent_tip', + LoadEvent.favorite: 'empty_favorite_tip', + LoadEvent.lan: 'empty_lan_tip', + LoadEvent.addressBook: 'empty_address_book_tip', + }); + final space = (isDesktop || isWebDesktop) ? 12.0 : 8.0; + final _curPeers = {}; + var _lastChangeTime = DateTime.now(); + var _lastQueryPeers = {}; + var _lastQueryTime = DateTime.now(); + var _lastWindowRestoreTime = DateTime.now(); + var _queryCount = 0; + var _exit = false; + bool _isActive = true; + + final _scrollController = ScrollController(); + + _PeersViewState() { + _startCheckOnlines(); + } + + @override + void initState() { + windowManager.addListener(this); + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + WidgetsBinding.instance.removeObserver(this); + _exit = true; + super.dispose(); + } + + @override + void onWindowFocus() { + _queryCount = 0; + _isActive = true; + } + + @override + void onWindowBlur() { + // We need this comparison because window restore (on Windows) also triggers `onWindowBlur()`. + // Maybe it's a bug of the window manager, but the source code seems to be correct. + // + // Although `onWindowRestore()` is called after `onWindowBlur()` in my test, + // we need the following comparison to ensure that `_isActive` is true in the end. + if (isWindows && + DateTime.now().difference(_lastWindowRestoreTime) < + const Duration(milliseconds: 300)) { + return; + } + _queryCount = _maxQueryCount; + _isActive = false; + } + + @override + void onWindowRestore() { + // Window restore (on MacOS and Linux) also triggers `onWindowFocus()`. + // But on Windows, it triggers `onWindowBlur()`, mybe it's a bug of the window manager. + if (!isWindows) return; + _queryCount = 0; + _isActive = true; + _lastWindowRestoreTime = DateTime.now(); + } + + @override + void onWindowMinimize() { + // Window minimize also triggers `onWindowBlur()`. + } + + // This function is required for mobile. + // `onWindowFocus` works fine for desktop. + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (isDesktop || isWebDesktop) return; + if (state == AppLifecycleState.resumed) { + _isActive = true; + _queryCount = 0; + } else if (state == AppLifecycleState.inactive) { + _isActive = false; + } + } + + @override + Widget build(BuildContext context) { + // We should avoid too many rebuilds. MacOS(m1, 14.6.1) on Flutter 3.19.6. + // Continious rebuilds of `ChangeNotifierProvider` will cause memory leak. + // Simple demo can reproduce this issue. + return ChangeNotifierProvider.value( + value: widget.peers, + child: Consumer(builder: (context, peers, child) { + if (peers.peers.isEmpty) { + gFFI.peerTabModel.setCurrentTabCachedPeers([]); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.sentiment_very_dissatisfied_rounded, + color: Theme.of(context).tabBarTheme.labelColor, + size: 40, + ).paddingOnly(bottom: 10), + Text( + translate( + _emptyMessages[widget.peers.loadEvent] ?? 'Empty', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).tabBarTheme.labelColor, + ), + ), + ], + ), + ); + } else { + return _buildPeersView(peers); + } + }), + ); + } + + onVisibilityChanged(VisibilityInfo info) { + final peerId = _peerId((info.key as ValueKey).value); + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + } + + String _cardId(String id) => widget.peers.name + id; + String _peerId(String cardId) => cardId.replaceAll(widget.peers.name, ''); + + Widget _buildPeersView(Peers peers) { + final updateEvent = peers.event; + final body = ObxValue((filters) { + return FutureBuilder>( + builder: (context, snapshot) { + if (snapshot.hasData) { + var peers = snapshot.data!; + if (peers.length > 1000) peers = peers.sublist(0, 1000); + gFFI.peerTabModel.setCurrentTabCachedPeers(peers); + buildOnePeer(Peer peer, bool isPortrait) { + final visibilityChild = VisibilityDetector( + key: ValueKey(_cardId(peer.id)), + onVisibilityChanged: onVisibilityChanged, + child: widget.peerCardBuilder(peer), + ); + // `Provider.of(context)` will causes infinete loop. + // Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`. + // + // No need to listen the currentTab change event. + // Because the currentTab change event will trigger the peers change event, + // and the peers change event will trigger _buildPeersView(). + return !isPortrait + ? Obx(() => peerCardUiType.value == PeerUiType.list + ? Container(height: 45, child: visibilityChild) + : peerCardUiType.value == PeerUiType.grid + ? SizedBox( + width: 220, height: 140, child: visibilityChild) + : SizedBox( + width: 220, height: 42, child: visibilityChild)) + : Container(child: visibilityChild); + } + + // We should avoid too many rebuilds. Win10(Some machines) on Flutter 3.19.6. + // Continious rebuilds of `ListView.builder` will cause memory leak. + // Simple demo can reproduce this issue. + final Widget child = Obx(() => stateGlobal.isPortrait.isTrue + ? ListView.builder( + itemCount: peers.length, + itemBuilder: (BuildContext context, int index) { + return buildOnePeer(peers[index], true).marginOnly( + top: index == 0 ? 0 : space / 2, bottom: space / 2); + }, + ) + : peerCardUiType.value == PeerUiType.list + ? ListView.builder( + controller: _scrollController, + itemCount: peers.length, + itemBuilder: (BuildContext context, int index) { + return buildOnePeer(peers[index], false).marginOnly( + right: space, + top: index == 0 ? 0 : space / 2, + bottom: space / 2); + }, + ) + : DynamicGridView.builder( + gridDelegate: SliverGridDelegateWithWrapping( + mainAxisSpacing: space / 2, + crossAxisSpacing: space), + itemCount: peers.length, + itemBuilder: (BuildContext context, int index) { + return buildOnePeer(peers[index], false); + })); + + if (updateEvent == UpdateEvent.load) { + _curPeers.clear(); + _curPeers.addAll(peers.map((e) => e.id)); + _queryOnlines(true); + } + return child; + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + future: matchPeers(filters[0].value, filters[1].value, peers.peers), + ); + }, obslist); + + return body; + } + + var _queryInterval = const Duration(seconds: 20); + + void _startCheckOnlines() { + () async { + final p = await bind.mainIsUsingPublicServer(); + if (!p) { + _queryInterval = const Duration(seconds: 6); + } + while (!_exit) { + final now = DateTime.now(); + if (!setEquals(_curPeers, _lastQueryPeers)) { + if (now.difference(_lastChangeTime) > const Duration(seconds: 1)) { + _queryOnlines(false); + } + } else { + final skipIfIsWeb = + isWeb && !(stateGlobal.isWebVisible && stateGlobal.isInMainPage); + final skipIfMobile = + (isAndroid || isIOS) && !stateGlobal.isInMainPage; + final skipIfNotActive = skipIfIsWeb || skipIfMobile || !_isActive; + if (!skipIfNotActive && (_queryCount < _maxQueryCount || !p)) { + if (now.difference(_lastQueryTime) >= _queryInterval) { + if (_curPeers.isNotEmpty) { + bind.queryOnlines(ids: _curPeers.toList(growable: false)); + _lastQueryTime = DateTime.now(); + _queryCount += 1; + } + } + } + } + await Future.delayed(const Duration(milliseconds: 300)); + } + }(); + } + + _queryOnlines(bool isLoadEvent) { + if (_curPeers.isNotEmpty) { + bind.queryOnlines(ids: _curPeers.toList(growable: false)); + _queryCount = 0; + } + _lastQueryPeers = {..._curPeers}; + if (isLoadEvent) { + _lastChangeTime = DateTime.now(); + } else { + _lastQueryTime = DateTime.now().subtract(_queryInterval); + } + } + + Future>? matchPeers( + String searchText, String sortedBy, List peers) async { + if (widget.peerFilter != null) { + peers = peers.where((peer) => widget.peerFilter!(peer)).toList(); + } + + // fallback to id sorting + if (!PeerSortType.values.contains(sortedBy)) { + sortedBy = PeerSortType.remoteId; + bind.setLocalFlutterOption( + k: kOptionPeerSorting, + v: sortedBy, + ); + } + + if (widget.peers.loadEvent != LoadEvent.recent) { + switch (sortedBy) { + case PeerSortType.remoteId: + peers.sort((p1, p2) => p1.getId().compareTo(p2.getId())); + break; + case PeerSortType.remoteHost: + peers.sort((p1, p2) => + p1.hostname.toLowerCase().compareTo(p2.hostname.toLowerCase())); + break; + case PeerSortType.username: + peers.sort((p1, p2) => + p1.username.toLowerCase().compareTo(p2.username.toLowerCase())); + break; + case PeerSortType.status: + peers.sort((p1, p2) => p1.online ? -1 : 1); + break; + } + } + + searchText = searchText.trim(); + if (searchText.isEmpty) { + return peers; + } + searchText = searchText.toLowerCase(); + final matches = await Future.wait( + peers.map((peer) => matchPeer(searchText, peer, widget.peerTabIndex))); + final filteredList = List.empty(growable: true); + for (var i = 0; i < peers.length; i++) { + if (matches[i]) { + filteredList.add(peers[i]); + } + } + + return filteredList; + } +} + +abstract class BasePeersView extends StatelessWidget { + final PeerTabIndex peerTabIndex; + final PeerFilter? peerFilter; + final PeerCardBuilder peerCardBuilder; + + const BasePeersView({ + Key? key, + required this.peerTabIndex, + this.peerFilter, + required this.peerCardBuilder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Peers peers; + switch (peerTabIndex) { + case PeerTabIndex.recent: + peers = gFFI.recentPeersModel; + break; + case PeerTabIndex.fav: + peers = gFFI.favoritePeersModel; + break; + case PeerTabIndex.lan: + peers = gFFI.lanPeersModel; + break; + case PeerTabIndex.ab: + peers = gFFI.abModel.peersModel; + break; + case PeerTabIndex.group: + peers = gFFI.groupModel.peersModel; + break; + } + return _PeersView( + peers: peers, + peerFilter: peerFilter, + peerCardBuilder: peerCardBuilder, + peerTabIndex: peerTabIndex); + } +} + +class RecentPeersView extends BasePeersView { + RecentPeersView( + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) + : super( + key: key, + peerTabIndex: PeerTabIndex.recent, + peerCardBuilder: (Peer peer) => RecentPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + ); + + @override + Widget build(BuildContext context) { + final widget = super.build(context); + bind.mainLoadRecentPeers(); + return widget; + } +} + +class FavoritePeersView extends BasePeersView { + FavoritePeersView( + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) + : super( + key: key, + peerTabIndex: PeerTabIndex.fav, + peerCardBuilder: (Peer peer) => FavoritePeerCard( + peer: peer, + menuPadding: menuPadding, + ), + ); + + @override + Widget build(BuildContext context) { + final widget = super.build(context); + bind.mainLoadFavPeers(); + return widget; + } +} + +class DiscoveredPeersView extends BasePeersView { + DiscoveredPeersView( + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) + : super( + key: key, + peerTabIndex: PeerTabIndex.lan, + peerCardBuilder: (Peer peer) => DiscoveredPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + ); + + @override + Widget build(BuildContext context) { + final widget = super.build(context); + bind.mainLoadLanPeers(); + bind.mainDiscover(); + return widget; + } +} + +class AddressBookPeersView extends BasePeersView { + AddressBookPeersView( + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) + : super( + key: key, + peerTabIndex: PeerTabIndex.ab, + peerFilter: (Peer peer) => + _hitTag(gFFI.abModel.selectedTags, peer.tags), + peerCardBuilder: (Peer peer) => AddressBookPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + ); + + static bool _hitTag(List selectedTags, List idents) { + if (selectedTags.isEmpty) { + return true; + } + // The result of a no-tag union with normal tags, still allows normal tags to perform union or intersection operations. + final selectedNormalTags = + selectedTags.where((tag) => tag != kUntagged).toList(); + if (selectedTags.contains(kUntagged)) { + if (idents.isEmpty) return true; + if (selectedNormalTags.isEmpty) return false; + } + if (gFFI.abModel.filterByIntersection.value) { + for (final tag in selectedNormalTags) { + if (!idents.contains(tag)) { + return false; + } + } + return true; + } else { + for (final tag in selectedNormalTags) { + if (idents.contains(tag)) { + return true; + } + } + return false; + } + } +} + +class MyGroupPeerView extends BasePeersView { + MyGroupPeerView( + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) + : super( + key: key, + peerTabIndex: PeerTabIndex.group, + peerFilter: filter, + peerCardBuilder: (Peer peer) => MyGroupPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + ); + + static bool filter(Peer peer) { + final model = gFFI.groupModel; + if (model.searchAccessibleItemNameText.isNotEmpty) { + final text = model.searchAccessibleItemNameText.value.toLowerCase(); + final searchPeersOfUser = model.users.any((user) => + user.name == peer.loginName && + (user.name.toLowerCase().contains(text) || + user.displayNameOrName.toLowerCase().contains(text))); + final searchPeersOfDeviceGroup = + peer.device_group_name.toLowerCase().contains(text) && + model.deviceGroups.any((g) => g.name == peer.device_group_name); + if (!searchPeersOfUser && !searchPeersOfDeviceGroup) { + return false; + } + } + if (model.selectedAccessibleItemName.isNotEmpty) { + if (model.isSelectedDeviceGroup.value) { + if (model.selectedAccessibleItemName.value != peer.device_group_name) { + return false; + } + } else { + if (model.selectedAccessibleItemName.value != peer.loginName) { + return false; + } + } + } + return true; + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/remote_input.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/remote_input.dart new file mode 100644 index 0000000..e35da64 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/remote_input.dart @@ -0,0 +1,680 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/gestures.dart'; + +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/input_model.dart'; + +import './gestures.dart'; + +class RawKeyFocusScope extends StatelessWidget { + final FocusNode? focusNode; + final ValueChanged? onFocusChange; + final InputModel inputModel; + final Widget child; + + RawKeyFocusScope({ + this.focusNode, + this.onFocusChange, + required this.inputModel, + required this.child, + }); + + @override + Widget build(BuildContext context) { + // https://github.com/flutter/flutter/issues/154053 + final useRawKeyEvents = isLinux && !isWeb; + // FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events, + // while `Alt` and `Control` are seperated key events for en-US input method. + return FocusScope( + autofocus: true, + child: Focus( + autofocus: true, + canRequestFocus: true, + focusNode: focusNode, + onFocusChange: onFocusChange, + onKey: useRawKeyEvents + ? (FocusNode data, RawKeyEvent event) => + inputModel.handleRawKeyEvent(event) + : null, + onKeyEvent: useRawKeyEvents + ? null + : (FocusNode node, KeyEvent event) => + inputModel.handleKeyEvent(event), + child: child)); + } +} + +// For virtual mouse when using the mouse mode on mobile. +// Special hold-drag mode: one finger holds a button (left/right button), another finger pans. +// This flag is to override the scale gesture to a pan gesture. +bool isSpecialHoldDragActive = false; +// Cache the last focal point to calculate deltas in special hold-drag mode. +Offset _lastSpecialHoldDragFocalPoint = Offset.zero; + +class RawTouchGestureDetectorRegion extends StatefulWidget { + final Widget child; + final FFI ffi; + final bool isCamera; + late final InputModel inputModel = ffi.inputModel; + late final FfiModel ffiModel = ffi.ffiModel; + + RawTouchGestureDetectorRegion({ + required this.child, + required this.ffi, + this.isCamera = false, + }); + + @override + State createState() => + _RawTouchGestureDetectorRegionState(); +} + +/// 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 +class _RawTouchGestureDetectorRegionState + extends State { + Offset _cacheLongPressPosition = Offset(0, 0); + // Timestamp of the last long press event. + int _cacheLongPressPositionTs = 0; + double _mouseScrollIntegral = 0; // mouse scroll speed controller + double _scale = 1; + + // Workaround tap down event when two fingers are used to scale(mobile) + TapDownDetails? _lastTapDownDetails; + + PointerDeviceKind? lastDeviceKind; + + // For touch mode, onDoubleTap + // `onDoubleTap()` does not provide the position of the tap event. + Offset _lastPosOfDoubleTapDown = Offset.zero; + bool _touchModePanStarted = false; + Offset _doubleFinerTapPosition = Offset.zero; + + // For mouse mode, we need to block the events when the cursor is in a blocked area. + // So we need to cache the last tap down position. + Offset? _lastTapDownPositionForMouseMode; + // Cache global position for onTap (which lacks position info). + Offset? _lastTapDownGlobalPosition; + + FFI get ffi => widget.ffi; + FfiModel get ffiModel => widget.ffiModel; + InputModel get inputModel => widget.inputModel; + bool get handleTouch => (isDesktop || isWebDesktop) || ffiModel.touchMode; + SessionID get sessionId => ffi.sessionId; + + @override + Widget build(BuildContext context) { + return RawGestureDetector( + child: widget.child, + gestures: makeGestures(context), + ); + } + + bool isNotTouchBasedDevice() { + return !kTouchBasedDeviceKinds.contains(lastDeviceKind); + } + + // Mobile, mouse mode. + // Check if should block the mouse tap event (`_lastTapDownPositionForMouseMode`). + bool shouldBlockMouseModeEvent() { + return _lastTapDownPositionForMouseMode != null && + ffi.cursorModel.shouldBlock(_lastTapDownPositionForMouseMode!.dx, + _lastTapDownPositionForMouseMode!.dy); + } + + onTapDown(TapDownDetails d) async { + lastDeviceKind = d.kind; + _lastTapDownGlobalPosition = d.globalPosition; + if (isNotTouchBasedDevice()) { + return; + } + if (handleTouch) { + _lastPosOfDoubleTapDown = d.localPosition; + // Desktop or mobile "Touch mode" + _lastTapDownDetails = d; + } else { + _lastTapDownPositionForMouseMode = d.localPosition; + } + } + + onTapUp(TapUpDetails d) async { + final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; + _lastTapDownDetails = null; + if (isNotTouchBasedDevice()) { + return; + } + // Filter duplicate touch tap events on iOS (Magic Mouse issue). + if (inputModel.shouldIgnoreTouchTap(d.globalPosition)) { + return; + } + if (handleTouch) { + final isMoved = + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + if (isMoved) { + // If pan already handled 'down', don't send it again. + if (lastTapDownDetails != null && !_touchModePanStarted) { + await inputModel.tapDown(MouseButtons.left); + } + await inputModel.tapUp(MouseButtons.left); + } + } + } + + onTap() async { + if (isNotTouchBasedDevice()) { + return; + } + // Filter duplicate touch tap events on iOS (Magic Mouse issue). + final lastPos = _lastTapDownGlobalPosition; + if (lastPos != null && inputModel.shouldIgnoreTouchTap(lastPos)) { + return; + } + if (!handleTouch) { + // Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details. + // Using `_lastTapDownPositionForMouseMode` instead. + if (shouldBlockMouseModeEvent()) { + return; + } + // Mobile, "Mouse mode" + await inputModel.tap(MouseButtons.left); + } + } + + onDoubleTapDown(TapDownDetails d) async { + lastDeviceKind = d.kind; + if (isNotTouchBasedDevice()) { + return; + } + if (handleTouch) { + _lastPosOfDoubleTapDown = d.localPosition; + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + } else { + _lastTapDownPositionForMouseMode = d.localPosition; + } + } + + onDoubleTap() async { + if (isNotTouchBasedDevice()) { + return; + } + if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) { + return; + } + if (handleTouch && + !ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) { + return; + } + // Check if the position is in a blocked area when using the mouse mode. + if (!handleTouch) { + if (shouldBlockMouseModeEvent()) { + return; + } + } + await inputModel.tap(MouseButtons.left); + await inputModel.tap(MouseButtons.left); + } + + onLongPressDown(LongPressDownDetails d) async { + lastDeviceKind = d.kind; + if (isNotTouchBasedDevice()) { + return; + } + if (handleTouch) { + _lastPosOfDoubleTapDown = d.localPosition; + _cacheLongPressPosition = d.localPosition; + if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) { + return; + } + _cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch; + if (ffiModel.isPeerMobile) { + await ffi.cursorModel + .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); + await inputModel.tapDown(MouseButtons.left); + } + } else { + _lastTapDownPositionForMouseMode = d.localPosition; + } + } + + onLongPressUp() async { + if (isNotTouchBasedDevice()) { + return; + } + if (handleTouch) { + await inputModel.tapUp(MouseButtons.left); + } + } + + // for mobiles + onLongPress() async { + if (isNotTouchBasedDevice()) { + return; + } + if (!ffi.ffiModel.isPeerMobile) { + if (handleTouch) { + final isMoved = await ffi.cursorModel + .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); + if (!isMoved) { + return; + } + } else { + if (shouldBlockMouseModeEvent()) { + return; + } + } + await inputModel.tap(MouseButtons.right); + } else { + // It's better to send a message to tell the controlled device that the long press event is triggered. + // We're now using a `TimerTask` in `InputService.kt` to decide whether to trigger the long press event. + // It's not accurate and it's better to use the same detection logic in the controlling side. + } + } + + onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async { + if (!ffiModel.isPeerMobile || isNotTouchBasedDevice()) { + return; + } + if (handleTouch) { + if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) { + return; + } + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + } + } + + onDoubleFinerTapDown(TapDownDetails d) async { + lastDeviceKind = d.kind; + if (isNotTouchBasedDevice()) { + return; + } + _doubleFinerTapPosition = d.localPosition; + // ignore for desktop and mobile + } + + onDoubleFinerTap(TapDownDetails d) async { + lastDeviceKind = d.kind; + if (isNotTouchBasedDevice()) { + return; + } + + // mobile mouse mode or desktop touch screen + final isMobileMouseMode = isMobile && !ffiModel.touchMode; + // We can't use `d.localPosition` here because it's always (0, 0) on desktop. + final isDesktopInRemoteRect = (isDesktop || isWebDesktop) && + ffi.cursorModel.isInRemoteRect(_doubleFinerTapPosition); + if (isMobileMouseMode || isDesktopInRemoteRect) { + await inputModel.tap(MouseButtons.right); + } + } + + onHoldDragStart(DragStartDetails d) async { + lastDeviceKind = d.kind; + if (isNotTouchBasedDevice()) { + return; + } + if (!handleTouch) { + if (isSpecialHoldDragActive) return; + await inputModel.sendMouse('down', MouseButtons.left); + } + } + + onHoldDragUpdate(DragUpdateDetails d) async { + if (isNotTouchBasedDevice()) { + return; + } + if (!handleTouch) { + if (isSpecialHoldDragActive) return; + await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); + } + } + + onHoldDragEnd(DragEndDetails d) async { + if (isNotTouchBasedDevice()) { + return; + } + if (!handleTouch) { + await inputModel.sendMouse('up', MouseButtons.left); + } + } + + onOneFingerPanStart(BuildContext context, DragStartDetails d) async { + final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; + _lastTapDownDetails = null; + lastDeviceKind = d.kind ?? lastDeviceKind; + if (isNotTouchBasedDevice()) { + return; + } + if (handleTouch) { + if (lastTapDownDetails != null) { + await ffi.cursorModel.move(lastTapDownDetails.localPosition.dx, + lastTapDownDetails.localPosition.dy); + } + if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { + return; + } + if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) { + return; + } + + _touchModePanStarted = true; + if (isDesktop || isWebDesktop) { + ffi.cursorModel.trySetRemoteWindowCoords(); + } + + // Workaround for the issue that the first pan event is sent a long time after the start event. + // If the time interval between the start event and the first pan event is less than 500ms, + // we consider to use the long press position as the start position. + // + // TODO: We should find a better way to send the first pan event as soon as possible. + if (DateTime.now().millisecondsSinceEpoch - _cacheLongPressPositionTs < + 500) { + await ffi.cursorModel + .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); + } + // In relative mouse mode, skip mouse down - only send movement via sendMobileRelativeMouseMove + if (!inputModel.relativeMouseMode.value) { + await inputModel.sendMouse('down', MouseButtons.left); + } + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + } else { + final offset = ffi.cursorModel.offset; + final cursorX = offset.dx; + final cursorY = offset.dy; + final visible = + ffi.cursorModel.getVisibleRect().inflate(1); // extend edges + final size = MediaQueryData.fromView(View.of(context)).size; + if (!visible.contains(Offset(cursorX, cursorY))) { + await ffi.cursorModel.move(size.width / 2, size.height / 2); + } + } + } + + onOneFingerPanUpdate(DragUpdateDetails d) async { + if (isNotTouchBasedDevice()) { + return; + } + if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { + return; + } + if (handleTouch && !_touchModePanStarted) { + return; + } + // In relative mouse mode, send delta directly without position tracking. + if (inputModel.relativeMouseMode.value) { + await inputModel.sendMobileRelativeMouseMove(d.delta.dx, d.delta.dy); + } else { + await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); + } + } + + onOneFingerPanEnd(DragEndDetails d) async { + _touchModePanStarted = false; + if (isNotTouchBasedDevice()) { + return; + } + if (isDesktop || isWebDesktop) { + ffi.cursorModel.clearRemoteWindowCoords(); + } + if (handleTouch) { + // In relative mouse mode, skip mouse up - matches the skipped mouse down in onOneFingerPanStart + if (!inputModel.relativeMouseMode.value) { + await inputModel.sendMouse('up', MouseButtons.left); + } + } + } + + // Reset `_touchModePanStarted` if the one-finger pan gesture is cancelled + // or rejected by the gesture arena. Without this, the flag can remain + // stuck in the "started" state and cause issues such as the Magic Mouse + // double-click problem on iPad with magic mouse. + onOneFingerPanCancel() { + _touchModePanStarted = false; + } + + // scale + pan event + onTwoFingerScaleStart(ScaleStartDetails d) { + _lastTapDownDetails = null; + if (isNotTouchBasedDevice()) { + return; + } + if (isSpecialHoldDragActive) { + // Initialize the last focal point to calculate deltas manually. + _lastSpecialHoldDragFocalPoint = d.focalPoint; + } + } + + onTwoFingerScaleUpdate(ScaleUpdateDetails d) async { + if (isNotTouchBasedDevice()) { + return; + } + + // If in special drag mode, perform a pan instead of a scale. + if (isSpecialHoldDragActive) { + // Calculate delta manually to avoid the jumpy behavior. + final delta = d.focalPoint - _lastSpecialHoldDragFocalPoint; + _lastSpecialHoldDragFocalPoint = d.focalPoint; + await ffi.cursorModel.updatePan(delta * 2.0, d.focalPoint, handleTouch); + return; + } + + if ((isDesktop || isWebDesktop)) { + final scale = ((d.scale - _scale) * 1000).toInt(); + _scale = d.scale; + + if (scale != 0) { + if (widget.isCamera) return; + await bind.sessionSendPointer( + sessionId: sessionId, + msg: json.encode( + PointerEventToRust(kPointerEventKindTouch, 'scale', scale) + .toJson())); + } + } else { + // mobile + ffi.canvasModel.updateScale(d.scale / _scale, d.focalPoint); + _scale = d.scale; + ffi.canvasModel.panX(d.focalPointDelta.dx); + ffi.canvasModel.panY(d.focalPointDelta.dy); + } + } + + onTwoFingerScaleEnd(ScaleEndDetails d) async { + if (isNotTouchBasedDevice()) { + return; + } + if ((isDesktop || isWebDesktop)) { + if (widget.isCamera) return; + await bind.sessionSendPointer( + sessionId: sessionId, + msg: json.encode( + PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson())); + } else { + // mobile + _scale = 1; + // No idea why we need to set the view style to "" here. + // bind.sessionSetViewStyle(sessionId: sessionId, value: ""); + } + if (!isSpecialHoldDragActive) { + await inputModel.sendMouse('up', MouseButtons.left); + } + } + + get onHoldDragCancel => null; + get onThreeFingerVerticalDragUpdate => ffi.ffiModel.isPeerAndroid + ? null + : (d) { + _mouseScrollIntegral += d.delta.dy / 4; + if (_mouseScrollIntegral > 1) { + inputModel.scroll(1); + _mouseScrollIntegral = 0; + } else if (_mouseScrollIntegral < -1) { + inputModel.scroll(-1); + _mouseScrollIntegral = 0; + } + }; + + makeGestures(BuildContext context) { + return { + // Official + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), (instance) { + instance + ..onTapDown = onTapDown + ..onTapUp = onTapUp + ..onTap = onTap; + }), + DoubleTapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => DoubleTapGestureRecognizer(), (instance) { + instance + ..onDoubleTapDown = onDoubleTapDown + ..onDoubleTap = onDoubleTap; + }), + LongPressGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(), (instance) { + instance + ..onLongPressDown = onLongPressDown + ..onLongPressUp = onLongPressUp + ..onLongPress = onLongPress + ..onLongPressMoveUpdate = onLongPressMoveUpdate; + }), + // Customized + HoldTapMoveGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => HoldTapMoveGestureRecognizer(), + (instance) => instance + ..onHoldDragStart = onHoldDragStart + ..onHoldDragUpdate = onHoldDragUpdate + ..onHoldDragCancel = onHoldDragCancel + ..onHoldDragEnd = onHoldDragEnd), + DoubleFinerTapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => DoubleFinerTapGestureRecognizer(), (instance) { + instance + ..onDoubleFinerTap = onDoubleFinerTap + ..onDoubleFinerTapDown = onDoubleFinerTapDown; + }), + CustomTouchGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => CustomTouchGestureRecognizer(), (instance) { + instance.onOneFingerPanStart = + (DragStartDetails d) => onOneFingerPanStart(context, d); + instance + ..onOneFingerPanUpdate = onOneFingerPanUpdate + ..onOneFingerPanEnd = onOneFingerPanEnd + ..onOneFingerPanCancel = onOneFingerPanCancel + ..onTwoFingerScaleStart = onTwoFingerScaleStart + ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate + ..onTwoFingerScaleEnd = onTwoFingerScaleEnd + ..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate; + }), + }; + } +} + +class RawPointerMouseRegion extends StatelessWidget { + final InputModel inputModel; + final Widget child; + final MouseCursor? cursor; + final PointerEnterEventListener? onEnter; + final PointerExitEventListener? onExit; + final PointerDownEventListener? onPointerDown; + final PointerUpEventListener? onPointerUp; + + RawPointerMouseRegion({ + this.onEnter, + this.onExit, + this.cursor, + this.onPointerDown, + this.onPointerUp, + required this.inputModel, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerHover: inputModel.onPointHoverImage, + onPointerDown: (evt) { + onPointerDown?.call(evt); + inputModel.onPointDownImage(evt); + }, + onPointerUp: (evt) { + onPointerUp?.call(evt); + inputModel.onPointUpImage(evt); + }, + onPointerMove: inputModel.onPointMoveImage, + onPointerSignal: inputModel.onPointerSignalImage, + onPointerPanZoomStart: inputModel.onPointerPanZoomStart, + onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate, + onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd, + child: MouseRegion( + cursor: inputModel.isViewOnly + ? MouseCursor.defer + : (cursor ?? MouseCursor.defer), + onEnter: onEnter, + onExit: onExit, + child: child, + ), + ); + } +} + +class CameraRawPointerMouseRegion extends StatelessWidget { + final InputModel inputModel; + final Widget child; + final PointerEnterEventListener? onEnter; + final PointerExitEventListener? onExit; + final PointerDownEventListener? onPointerDown; + final PointerUpEventListener? onPointerUp; + + CameraRawPointerMouseRegion({ + this.onEnter, + this.onExit, + this.onPointerDown, + this.onPointerUp, + required this.inputModel, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerHover: (evt) { + final offset = evt.position; + double x = offset.dx; + double y = max(0.0, offset.dy); + inputModel.handlePointerDevicePos( + kPointerEventKindMouse, x, y, true, kMouseEventTypeDefault); + }, + onPointerDown: (evt) { + onPointerDown?.call(evt); + }, + onPointerUp: (evt) { + onPointerUp?.call(evt); + }, + child: MouseRegion( + cursor: MouseCursor.defer, + onEnter: onEnter, + onExit: onExit, + child: child, + ), + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/setting_widgets.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/setting_widgets.dart new file mode 100644 index 0000000..f3be770 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/setting_widgets.dart @@ -0,0 +1,340 @@ +import 'package:debounce_throttle/debounce_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; + +customImageQualityWidget( + {required double initQuality, + required double initFps, + required Function(double)? setQuality, + required Function(double)? setFps, + required bool showFps, + required bool showMoreQuality}) { + if (initQuality < kMinQuality || + initQuality > (showMoreQuality ? kMaxMoreQuality : kMaxQuality)) { + initQuality = kDefaultQuality; + } + if (initFps < kMinFps || initFps > kMaxFps) { + initFps = kDefaultFps; + } + final qualityValue = initQuality.obs; + final fpsValue = initFps.obs; + + final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality); + final debouncerQuality = Debouncer( + Duration(milliseconds: 1000), + onChanged: setQuality, + initialValue: qualityValue.value, + ); + final debouncerFps = Debouncer( + Duration(milliseconds: 1000), + onChanged: setFps, + initialValue: fpsValue.value, + ); + + onMoreChanged(bool? value) { + if (value == null) return; + moreQualityChecked.value = value; + if (!value && qualityValue.value > 100) { + qualityValue.value = 100; + } + debouncerQuality.value = qualityValue.value; + } + + return Column( + children: [ + Obx(() => Row( + children: [ + Expanded( + flex: 3, + child: Slider( + value: qualityValue.value, + min: kMinQuality, + max: moreQualityChecked.value ? kMaxMoreQuality : kMaxQuality, + divisions: moreQualityChecked.value + ? ((kMaxMoreQuality - kMinQuality) / 10).round() + : ((kMaxQuality - kMinQuality) / 5).round(), + onChanged: setQuality == null + ? null + : (double value) async { + qualityValue.value = value; + debouncerQuality.value = value; + }, + ), + ), + Expanded( + flex: 1, + child: Text( + '${qualityValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + Expanded( + flex: isMobile ? 2 : 1, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )), + // mobile doesn't have enough space + if (showMoreQuality && !isMobile) + Expanded( + flex: 1, + child: Row( + children: [ + Checkbox( + value: moreQualityChecked.value, + onChanged: onMoreChanged, + ), + Expanded( + child: Text(translate('More')), + ) + ], + )) + ], + )), + if (showMoreQuality && isMobile) + Obx(() => Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Checkbox( + value: moreQualityChecked.value, + onChanged: onMoreChanged, + ), + ), + ), + Expanded( + child: Text(translate('More')), + ) + ], + )), + if (showFps) + Obx(() => Row( + children: [ + Expanded( + flex: 3, + child: Slider( + value: fpsValue.value, + min: kMinFps, + max: kMaxFps, + divisions: ((kMaxFps - kMinFps) / 5).round(), + onChanged: setFps == null + ? null + : (double value) async { + fpsValue.value = value; + debouncerFps.value = value; + }, + ), + ), + Expanded( + flex: 1, + child: Text( + '${fpsValue.value.round()}', + style: const TextStyle(fontSize: 15), + )), + Expanded( + flex: 2, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + )), + ], + ); +} + +customImageQualitySetting() { + final qualityKey = 'custom_image_quality'; + final fpsKey = 'custom-fps'; + + final initQuality = + (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? + kDefaultQuality); + final isQuanlityFixed = isOptionFixed(qualityKey); + final initFps = + (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? + kDefaultFps); + final isFpsFixed = isOptionFixed(fpsKey); + + return customImageQualityWidget( + initQuality: initQuality, + initFps: initFps, + setQuality: isQuanlityFixed + ? null + : (v) { + bind.mainSetUserDefaultOption( + key: qualityKey, value: v.toString()); + }, + setFps: isFpsFixed + ? null + : (v) { + bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString()); + }, + showFps: true, + showMoreQuality: true); +} + +List ServerConfigImportExportWidgets( + List controllers, + List errMsgs, +) { + import() { + Clipboard.getData(Clipboard.kTextPlain).then((value) { + importConfig(controllers, errMsgs, value?.text); + }); + } + + export() { + final text = ServerConfig( + idServer: controllers[0].text.trim(), + relayServer: controllers[1].text.trim(), + apiServer: controllers[2].text.trim(), + key: controllers[3].text.trim()) + .encode(); + debugPrint("ServerConfig export: $text"); + Clipboard.setData(ClipboardData(text: text)); + showToast(translate('Export server configuration successfully')); + } + + return [ + Tooltip( + message: translate('Import server config'), + child: IconButton( + icon: Icon(Icons.paste, color: Colors.grey), onPressed: import), + ), + Tooltip( + message: translate('Export Server Config'), + child: IconButton( + icon: Icon(Icons.copy, color: Colors.grey), onPressed: export)) + ]; +} + +List<(String, String)> otherDefaultSettings() { + List<(String, String)> v = [ + ('View Mode', kOptionViewOnly), + if ((isDesktop || isWebDesktop)) + ('show_monitors_tip', kKeyShowMonitorsToolbar), + if ((isDesktop || isWebDesktop)) + ('Collapse toolbar', kOptionCollapseToolbar), + ('Show remote cursor', kOptionShowRemoteCursor), + ('Follow remote cursor', kOptionFollowRemoteCursor), + ('Follow remote window focus', kOptionFollowRemoteWindow), + if ((isDesktop || isWebDesktop)) ('Zoom cursor', kOptionZoomCursor), + ('Show quality monitor', kOptionShowQualityMonitor), + ('Mute', kOptionDisableAudio), + if (isDesktop) ('Enable file copy and paste', kOptionEnableFileCopyPaste), + ('Disable clipboard', kOptionDisableClipboard), + ('Lock after session end', kOptionLockAfterSessionEnd), + ('Privacy mode', kOptionPrivacyMode), + ('True color (4:4:4)', kOptionI444), + ('Reverse mouse wheel', kKeyReverseMouseWheel), + ('swap-left-right-mouse', kOptionSwapLeftRightMouse), + if (isDesktop) + ( + 'Show displays as individual windows', + kKeyShowDisplaysAsIndividualWindows + ), + if (isDesktop) + ( + 'Use all my displays for the remote session', + kKeyUseAllMyDisplaysForTheRemoteSession + ), + ('Keep terminal sessions on disconnect', kOptionTerminalPersistent), + ]; + + return v; +} + +class TrackpadSpeedWidget extends StatefulWidget { + final SimpleWrapper value; + // If null, no debouncer will be applied. + final Function(int)? onDebouncer; + + TrackpadSpeedWidget({Key? key, required this.value, this.onDebouncer}); + + @override + TrackpadSpeedWidgetState createState() => TrackpadSpeedWidgetState(); +} + +class TrackpadSpeedWidgetState extends State { + final TextEditingController _controller = TextEditingController(); + late final Debouncer debouncerSpeed; + + set value(int v) => widget.value.value = v; + int get value => widget.value.value; + + void updateValue(int newValue) { + setState(() { + value = newValue.clamp(kMinTrackpadSpeed, kMaxTrackpadSpeed); + // Scale the trackpad speed value to a percentage for display purposes. + _controller.text = value.toString(); + if (widget.onDebouncer != null) { + debouncerSpeed.setValue(value); + } + }); + } + + @override + void initState() { + super.initState(); + debouncerSpeed = Debouncer( + Duration(milliseconds: 1000), + onChanged: widget.onDebouncer, + initialValue: widget.value.value, + ); + } + + @override + Widget build(BuildContext context) { + if (_controller.text.isEmpty) { + _controller.text = value.toString(); + } + return Row( + children: [ + Expanded( + flex: 3, + child: Slider( + value: value.toDouble(), + min: kMinTrackpadSpeed.toDouble(), + max: kMaxTrackpadSpeed.toDouble(), + divisions: ((kMaxTrackpadSpeed - kMinTrackpadSpeed) / 10).round(), + onChanged: (double v) => updateValue(v.round()), + ), + ), + Expanded( + flex: 1, + child: Row( + children: [ + SizedBox( + width: 56, + child: TextField( + controller: _controller, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + onSubmitted: (text) { + int? v = int.tryParse(text); + if (v != null) { + updateValue(v); + } + }, + style: const TextStyle(fontSize: 13), + decoration: InputDecoration( + contentPadding: + EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), + ), + ), + ).marginOnly(right: 8.0), + Text( + '%', + style: const TextStyle(fontSize: 15), + ) + ], + )), + ], + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/common/widgets/toolbar.dart b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/toolbar.dart new file mode 100644 index 0000000..a46ce54 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/common/widgets/toolbar.dart @@ -0,0 +1,1024 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; +import 'package:flutter_hbb/common/widgets/login.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +bool isEditOsPassword = false; + +class TTextMenu { + final Widget child; + final VoidCallback? onPressed; + Widget? trailingIcon; + bool divider; + TTextMenu( + {required this.child, + required this.onPressed, + this.trailingIcon, + this.divider = false}); + + Widget getChild() { + if (trailingIcon != null) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + child, + trailingIcon!, + ], + ); + } else { + return child; + } + } +} + +class TRadioMenu { + final Widget child; + final T value; + final T groupValue; + final ValueChanged? onChanged; + + TRadioMenu( + {required this.child, + required this.value, + required this.groupValue, + required this.onChanged}); +} + +class TToggleMenu { + final Widget child; + final bool value; + final ValueChanged? onChanged; + TToggleMenu( + {required this.child, required this.value, required this.onChanged}); +} + +handleOsPasswordEditIcon( + SessionID sessionId, OverlayDialogManager dialogManager) { + isEditOsPassword = true; + showSetOSPassword( + sessionId, false, dialogManager, null, () => isEditOsPassword = false); +} + +handleOsPasswordAction( + SessionID sessionId, OverlayDialogManager dialogManager) async { + if (isEditOsPassword) { + isEditOsPassword = false; + return; + } + final password = + await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ?? + ''; + if (password.isEmpty) { + showSetOSPassword(sessionId, true, dialogManager, password, + () => isEditOsPassword = false); + } else { + bind.sessionInputOsPassword(sessionId: sessionId, value: password); + } +} + +List toolbarControls(BuildContext context, String id, FFI ffi) { + final ffiModel = ffi.ffiModel; + final pi = ffiModel.pi; + final perms = ffiModel.permissions; + final sessionId = ffi.sessionId; + final isDefaultConn = ffi.connType == ConnType.defaultConn; + + List v = []; + // elevation + if (isDefaultConn && + perms['keyboard'] != false && + ffi.elevationModel.showRequestMenu) { + v.add( + TTextMenu( + child: Text(translate('Request Elevation')), + onPressed: () => + showRequestElevationDialog(sessionId, ffi.dialogManager)), + ); + } + // osAccount / osPassword + if (isDefaultConn && perms['keyboard'] != false) { + v.add( + TTextMenu( + child: Row(children: [ + Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')), + ]), + trailingIcon: Transform.scale( + scale: (isDesktop || isWebDesktop) ? 0.8 : 1, + child: IconButton( + onPressed: () { + if (isMobile && Navigator.canPop(context)) { + Navigator.pop(context); + } + if (pi.isHeadless) { + showSetOSAccount(sessionId, ffi.dialogManager); + } else { + handleOsPasswordEditIcon(sessionId, ffi.dialogManager); + } + }, + icon: Icon(Icons.edit, color: isMobile ? MyTheme.accent : null), + ), + ), + onPressed: () => pi.isHeadless + ? showSetOSAccount(sessionId, ffi.dialogManager) + : handleOsPasswordAction(sessionId, ffi.dialogManager), + ), + ); + } + // paste + if (isDefaultConn && + pi.platform != kPeerPlatformAndroid && + perms['keyboard'] != false) { + v.add(TTextMenu( + child: Text(translate('Send clipboard keystrokes')), + onPressed: () async { + ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + bind.sessionInputString( + sessionId: sessionId, value: data.text ?? ""); + } + })); + } + // reset canvas + if (isDefaultConn && isMobile) { + v.add(TTextMenu( + child: Text(translate('Reset canvas')), + onPressed: () => ffi.cursorModel.reset())); + } + + // https://github.com/rustdesk/rustdesk/pull/9731 + // Does not work for connection established by "accept". + connectWithToken( + {bool isFileTransfer = false, + bool isViewCamera = false, + bool isTcpTunneling = false, + bool isTerminal = false}) { + final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); + connect(context, id, + isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, + isTerminal: isTerminal, + isTcpTunneling: isTcpTunneling, + connToken: connToken); + } + + if (isDefaultConn && isDesktop) { + v.add( + TTextMenu( + child: Text(translate('Transfer file')), + onPressed: () => connectWithToken(isFileTransfer: true)), + ); + v.add( + TTextMenu( + child: Text(translate('View camera')), + onPressed: () => connectWithToken(isViewCamera: true)), + ); + v.add( + TTextMenu( + child: Text('${translate('Terminal')} (beta)'), + onPressed: () => connectWithToken(isTerminal: true)), + ); + v.add( + TTextMenu( + child: Text(translate('TCP tunneling')), + onPressed: () => connectWithToken(isTcpTunneling: true)), + ); + } + // note + if (isDefaultConn && !bind.isDisableAccount()) { + v.add( + TTextMenu( + child: Text(translate('Note')), + onPressed: () async { + bool isLogin = + bind.mainGetLocalOption(key: 'access_token').isNotEmpty; + if (!isLogin) { + final res = await loginDialog(); + if (res != true) return; + // Desktop: send message to main window to refresh login status + // Web: login is required before connection, so no need to refresh + // Mobile: same isolate, no need to send message + if (isDesktop) { + rustDeskWinManager.call( + WindowType.Main, kWindowRefreshCurrentUser, ""); + } + } + showAuditDialog(ffi); + }), + ); + } + // divider + if (isDefaultConn && (isDesktop || isWebDesktop)) { + v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true)); + } + // ctrlAltDel + if (isDefaultConn && + !ffiModel.viewOnly && + ffiModel.keyboard && + (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) { + v.add( + TTextMenu( + child: Text('${translate("Insert Ctrl + Alt + Del")}'), + onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)), + ); + } + // restart + if (isDefaultConn && + perms['restart'] != false && + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS)) { + v.add( + TTextMenu( + child: Text(translate('Restart remote device')), + onPressed: () => + showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)), + ); + } + // insertLock + if (isDefaultConn && !ffiModel.viewOnly && ffi.ffiModel.keyboard) { + v.add( + TTextMenu( + child: Text(translate('Insert Lock')), + onPressed: () => bind.sessionLockScreen(sessionId: sessionId)), + ); + } + // blockUserInput + if (isDefaultConn && + ffi.ffiModel.keyboard && + ffi.ffiModel.permissions['block_input'] != false && + pi.platform == kPeerPlatformWindows) // privacy-mode != true ?? + { + v.add(TTextMenu( + child: Obx(() => Text(translate( + '${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))), + onPressed: () { + RxBool blockInput = BlockInputState.find(id); + bind.sessionToggleOption( + sessionId: sessionId, + value: '${blockInput.value ? 'un' : ''}block-input'); + blockInput.value = !blockInput.value; + })); + } + // switchSides + if (isDefaultConn && + isDesktop && + ffiModel.keyboard && + pi.platform != kPeerPlatformAndroid && + pi.platform != kPeerPlatformMacOS && + versionCmp(pi.version, '1.2.0') >= 0 && + bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) { + v.add(TTextMenu( + child: Text(translate('Switch Sides')), + onPressed: () => + showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager))); + } + // refresh + if (pi.version.isNotEmpty) { + v.add(TTextMenu( + child: Text(translate('Refresh')), + onPressed: () => sessionRefreshVideo(sessionId, pi), + )); + } + // record + if (!(isDesktop || isWeb) && + (ffi.recordingModel.start || (perms["recording"] != false))) { + v.add(TTextMenu( + child: Row( + children: [ + Text(translate(ffi.recordingModel.start + ? 'Stop session recording' + : 'Start session recording')), + Padding( + padding: EdgeInsets.only(left: 12), + child: Icon( + ffi.recordingModel.start + ? Icons.pause_circle_filled + : Icons.videocam_outlined, + color: MyTheme.accent), + ) + ], + ), + onPressed: () => ffi.recordingModel.toggle())); + } + + // to-do: + // 1. Web desktop + // 2. Mobile, copy the image to the clipboard + if (isDesktop) { + final isScreenshotSupported = bind.sessionGetCommonSync( + sessionId: sessionId, key: 'is_screenshot_supported', param: ''); + if ('true' == isScreenshotSupported) { + v.add(TTextMenu( + child: Text(ffi.ffiModel.timerScreenshot != null + ? '${translate('Taking screenshot')} ...' + : translate('Take screenshot')), + onPressed: ffi.ffiModel.timerScreenshot != null + ? null + : () { + if (pi.currentDisplay == kAllDisplayValue) { + msgBox( + sessionId, + 'custom-nook-nocancel-hasclose-info', + 'Take screenshot', + 'screenshot-merged-screen-not-supported-tip', + '', + ffi.dialogManager); + } else { + bind.sessionTakeScreenshot( + sessionId: sessionId, display: pi.currentDisplay); + ffi.ffiModel.timerScreenshot = + Timer(Duration(seconds: 30), () { + ffi.ffiModel.timerScreenshot = null; + }); + } + }, + )); + } + } + // fingerprint + if (!(isDesktop || isWebDesktop)) { + v.add(TTextMenu( + child: Text(translate('Copy Fingerprint')), + onPressed: () => onCopyFingerprint(FingerprintState.find(id).value), + )); + } + return v; +} + +Future>> toolbarViewStyle( + BuildContext context, String id, FFI ffi) async { + final groupValue = + await bind.sessionGetViewStyle(sessionId: ffi.sessionId) ?? ''; + void onChanged(String? value) async { + if (value == null) return; + bind + .sessionSetViewStyle(sessionId: ffi.sessionId, value: value) + .then((_) => ffi.canvasModel.updateViewStyle()); + } + + return [ + TRadioMenu( + child: Text(translate('Scale original')), + value: kRemoteViewStyleOriginal, + groupValue: groupValue, + onChanged: onChanged), + TRadioMenu( + child: Text(translate('Scale adaptive')), + value: kRemoteViewStyleAdaptive, + groupValue: groupValue, + onChanged: onChanged), + TRadioMenu( + child: Text(translate('Scale custom')), + value: kRemoteViewStyleCustom, + groupValue: groupValue, + onChanged: onChanged) + ]; +} + +Future>> toolbarImageQuality( + BuildContext context, String id, FFI ffi) async { + final groupValue = + await bind.sessionGetImageQuality(sessionId: ffi.sessionId) ?? ''; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetImageQuality(sessionId: ffi.sessionId, value: value); + } + + return [ + TRadioMenu( + child: Text(translate('Good image quality')), + value: kRemoteImageQualityBest, + groupValue: groupValue, + onChanged: onChanged), + TRadioMenu( + child: Text(translate('Balanced')), + value: kRemoteImageQualityBalanced, + groupValue: groupValue, + onChanged: onChanged), + TRadioMenu( + child: Text(translate('Optimize reaction time')), + value: kRemoteImageQualityLow, + groupValue: groupValue, + onChanged: onChanged), + TRadioMenu( + child: Text(translate('Custom')), + value: kRemoteImageQualityCustom, + groupValue: groupValue, + onChanged: (value) { + onChanged(value); + customImageQualityDialog(ffi.sessionId, id, ffi); + }, + ), + ]; +} + +Future>> toolbarCodec( + BuildContext context, String id, FFI ffi) async { + final sessionId = ffi.sessionId; + final alternativeCodecs = + await bind.sessionAlternativeCodecs(sessionId: sessionId); + final groupValue = await bind.sessionGetOption( + sessionId: sessionId, arg: kOptionCodecPreference) ?? + ''; + final List codecs = []; + try { + final Map codecsJson = jsonDecode(alternativeCodecs); + final vp8 = codecsJson['vp8'] ?? false; + final av1 = codecsJson['av1'] ?? false; + final h264 = codecsJson['h264'] ?? false; + final h265 = codecsJson['h265'] ?? false; + codecs.add(vp8); + codecs.add(av1); + codecs.add(h264); + codecs.add(h265); + } catch (e) { + debugPrint("Show Codec Preference err=$e"); + } + final visible = + codecs.length == 4 && (codecs[0] || codecs[1] || codecs[2] || codecs[3]); + if (!visible) return []; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionPeerOption( + sessionId: sessionId, name: kOptionCodecPreference, value: value); + bind.sessionChangePreferCodec(sessionId: sessionId); + } + + TRadioMenu radio(String label, String value, bool enabled) { + return TRadioMenu( + child: Text(label), + value: value, + groupValue: groupValue, + onChanged: enabled ? onChanged : null); + } + + var autoLabel = translate('Auto'); + if (groupValue == 'auto' && + ffi.qualityMonitorModel.data.codecFormat != null) { + autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})'; + } + return [ + radio(autoLabel, 'auto', true), + if (codecs[0]) radio('VP8', 'vp8', codecs[0]), + radio('VP9', 'vp9', true), + if (codecs[1]) radio('AV1', 'av1', codecs[1]), + if (codecs[2]) radio('H264', 'h264', codecs[2]), + if (codecs[3]) radio('H265', 'h265', codecs[3]), + ]; +} + +Future> toolbarCursor( + BuildContext context, String id, FFI ffi) async { + List v = []; + final ffiModel = ffi.ffiModel; + final pi = ffiModel.pi; + final sessionId = ffi.sessionId; + + // show remote cursor + if (pi.platform != kPeerPlatformAndroid && + !ffi.canvasModel.cursorEmbedded && + !pi.isWayland) { + final state = ShowRemoteCursorState.find(id); + final lockState = ShowRemoteCursorLockState.find(id); + final enabled = !ffiModel.viewOnly; + final option = 'show-remote-cursor'; + if (pi.currentDisplay == kAllDisplayValue || + bind.sessionIsMultiUiSession(sessionId: sessionId)) { + lockState.value = false; + } + v.add(TToggleMenu( + child: Text(translate('Show remote cursor')), + value: state.value, + onChanged: enabled && !lockState.value + ? (value) async { + if (value == null) return; + await bind.sessionToggleOption( + sessionId: sessionId, value: option); + state.value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: option); + } + : null)); + } + // follow remote cursor + if (pi.platform != kPeerPlatformAndroid && + !ffi.canvasModel.cursorEmbedded && + !pi.isWayland && + versionCmp(pi.version, "1.2.4") >= 0 && + pi.displays.length > 1 && + pi.currentDisplay != kAllDisplayValue && + !bind.sessionIsMultiUiSession(sessionId: sessionId)) { + final option = 'follow-remote-cursor'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + final showCursorOption = 'show-remote-cursor'; + final showCursorState = ShowRemoteCursorState.find(id); + final showCursorLockState = ShowRemoteCursorLockState.find(id); + final showCursorEnabled = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: showCursorOption); + showCursorLockState.value = value; + if (value && !showCursorEnabled) { + await bind.sessionToggleOption( + sessionId: sessionId, value: showCursorOption); + showCursorState.value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: showCursorOption); + } + v.add(TToggleMenu( + child: Text(translate('Follow remote cursor')), + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(sessionId: sessionId, value: option); + value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: option); + showCursorLockState.value = value; + if (!showCursorEnabled) { + await bind.sessionToggleOption( + sessionId: sessionId, value: showCursorOption); + showCursorState.value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: showCursorOption); + } + })); + } + // follow remote window focus + if (pi.platform != kPeerPlatformAndroid && + !ffi.canvasModel.cursorEmbedded && + !pi.isWayland && + versionCmp(pi.version, "1.2.4") >= 0 && + pi.displays.length > 1 && + pi.currentDisplay != kAllDisplayValue && + !bind.sessionIsMultiUiSession(sessionId: sessionId)) { + final option = 'follow-remote-window'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + v.add(TToggleMenu( + child: Text(translate('Follow remote window focus')), + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(sessionId: sessionId, value: option); + value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: option); + })); + } + // zoom cursor + final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? ''; + if (!isMobile && + pi.platform != kPeerPlatformAndroid && + viewStyle != kRemoteViewStyleOriginal) { + final option = 'zoom-cursor'; + final peerState = PeerBoolOption.find(id, option); + v.add(TToggleMenu( + child: Text(translate('Zoom cursor')), + value: peerState.value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(sessionId: sessionId, value: option); + peerState.value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + }, + )); + } + return v; +} + +Future> toolbarDisplayToggle( + BuildContext context, String id, FFI ffi) async { + List v = []; + final ffiModel = ffi.ffiModel; + final pi = ffiModel.pi; + final perms = ffiModel.permissions; + final sessionId = ffi.sessionId; + final isDefaultConn = ffi.connType == ConnType.defaultConn; + + // show quality monitor + final option = 'show-quality-monitor'; + v.add(TToggleMenu( + value: bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option), + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(sessionId: sessionId, value: option); + ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); + }, + child: Text(translate('Show quality monitor')))); + // mute + if (isDefaultConn && perms['audio'] != false) { + final option = 'disable-audio'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + v.add(TToggleMenu( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(sessionId: sessionId, value: option); + }, + child: Text(translate('Mute')))); + } + // file copy and paste + // If the version is less than 1.2.4, file copy and paste is supported on Windows only. + final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 && + isWindows && + pi.platform == kPeerPlatformWindows; + // If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set. + final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 && + bind.mainHasFileClipboard() && + pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard); + if (isDefaultConn && + ffiModel.keyboard && + perms['file'] != false && + (isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) { + final enabled = !ffiModel.viewOnly; + final value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: kOptionEnableFileCopyPaste); + v.add(TToggleMenu( + value: value, + onChanged: enabled + ? (value) { + if (value == null) return; + bind.sessionToggleOption( + sessionId: sessionId, value: kOptionEnableFileCopyPaste); + } + : null, + child: Text(translate('Enable file copy and paste')))); + } + // disable clipboard + if (isDefaultConn && ffiModel.keyboard && perms['clipboard'] != false) { + final enabled = !ffiModel.viewOnly; + final option = 'disable-clipboard'; + var value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + if (ffiModel.viewOnly) value = true; + v.add(TToggleMenu( + value: value, + onChanged: enabled + ? (value) { + if (value == null) return; + bind.sessionToggleOption(sessionId: sessionId, value: option); + } + : null, + child: Text(translate('Disable clipboard')))); + } + // lock after session end + if (isDefaultConn && ffiModel.keyboard && !ffiModel.isPeerAndroid) { + final enabled = !ffiModel.viewOnly; + final option = 'lock-after-session-end'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + v.add(TToggleMenu( + value: value, + onChanged: enabled + ? (value) { + if (value == null) return; + bind.sessionToggleOption(sessionId: sessionId, value: option); + } + : null, + child: Text(translate('Lock after session end')))); + } + + if (pi.isSupportMultiDisplay && + PrivacyModeState.find(id).isEmpty && + pi.displaysCount.value > 1 && + bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') { + final value = + bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) == + 'Y'; + v.add(TToggleMenu( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionSetDisplaysAsIndividualWindows( + sessionId: sessionId, value: value ? 'Y' : 'N'); + }, + child: Text(translate('Show displays as individual windows')))); + } + + final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1; + if (pi.isSupportMultiDisplay && isMultiScreens) { + final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession( + sessionId: ffi.sessionId) == + 'Y'; + v.add(TToggleMenu( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionSetUseAllMyDisplaysForTheRemoteSession( + sessionId: sessionId, value: value ? 'Y' : 'N'); + }, + child: Text(translate('Use all my displays for the remote session')))); + } + + // 444 + final codec_format = ffi.qualityMonitorModel.data.codecFormat; + if (versionCmp(pi.version, "1.2.4") >= 0 && + (codec_format == "AV1" || codec_format == "VP9")) { + final option = 'i444'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + v.add(TToggleMenu( + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(sessionId: sessionId, value: option); + bind.sessionChangePreferCodec(sessionId: sessionId); + }, + child: Text(translate('True color (4:4:4)')))); + } + + if (isDefaultConn && isMobile) { + v.addAll(toolbarKeyboardToggles(ffi)); + } + + // view mode (mobile only, desktop is in keyboard menu) + if (isDefaultConn && isMobile && versionCmp(pi.version, '1.2.0') >= 0) { + v.add(TToggleMenu( + value: ffiModel.viewOnly, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption( + sessionId: ffi.sessionId, value: kOptionToggleViewOnly); + ffiModel.setViewOnly(id, value); + }, + child: Text(translate('View Mode')))); + } + return v; +} + +var togglePrivacyModeTime = DateTime.now().subtract(const Duration(hours: 1)); + +List toolbarPrivacyMode( + RxString privacyModeState, BuildContext context, String id, FFI ffi) { + final ffiModel = ffi.ffiModel; + final pi = ffiModel.pi; + final sessionId = ffi.sessionId; + + getDefaultMenu(Future Function(SessionID sid, String opt) toggleFunc) { + final enabled = !ffi.ffiModel.viewOnly; + return TToggleMenu( + value: privacyModeState.isNotEmpty, + onChanged: enabled + ? (value) { + if (value == null) return; + if (ffiModel.pi.currentDisplay != 0 && + ffiModel.pi.currentDisplay != kAllDisplayValue) { + msgBox( + sessionId, + 'custom-nook-nocancel-hasclose', + 'info', + 'Please switch to Display 1 first', + '', + ffi.dialogManager); + return; + } + final option = 'privacy-mode'; + toggleFunc(sessionId, option); + } + : null, + child: Text(translate('Privacy mode'))); + } + + final privacyModeImpls = + pi.platformAdditions[kPlatformAdditionsSupportedPrivacyModeImpl] + as List?; + if (privacyModeImpls == null) { + return [ + getDefaultMenu((sid, opt) async { + bind.sessionToggleOption(sessionId: sid, value: opt); + togglePrivacyModeTime = DateTime.now(); + }) + ]; + } + if (privacyModeImpls.isEmpty) { + return []; + } + + if (privacyModeImpls.length == 1) { + final implKey = (privacyModeImpls[0] as List)[0] as String; + return [ + getDefaultMenu((sid, opt) async { + bind.sessionTogglePrivacyMode( + sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty); + togglePrivacyModeTime = DateTime.now(); + }) + ]; + } else { + return privacyModeImpls.map((e) { + final implKey = (e as List)[0] as String; + final implName = (e)[1] as String; + return TToggleMenu( + child: Text(translate(implName)), + value: privacyModeState.value == implKey, + onChanged: (value) { + if (value == null) return; + togglePrivacyModeTime = DateTime.now(); + bind.sessionTogglePrivacyMode( + sessionId: sessionId, implKey: implKey, on: value); + }); + }).toList(); + } +} + +List toolbarKeyboardToggles(FFI ffi) { + final ffiModel = ffi.ffiModel; + final pi = ffiModel.pi; + final sessionId = ffi.sessionId; + final isDefaultConn = ffi.connType == ConnType.defaultConn; + List v = []; + + // swap key + if (ffiModel.keyboard && + ((isMacOS && pi.platform != kPeerPlatformMacOS) || + (!isMacOS && pi.platform == kPeerPlatformMacOS))) { + final option = 'allow_swap_key'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + onChanged(bool? value) { + if (value == null) return; + bind.sessionToggleOption(sessionId: sessionId, value: option); + } + + final enabled = !ffi.ffiModel.viewOnly; + v.add(TToggleMenu( + value: value, + onChanged: enabled ? onChanged : null, + child: Text(translate('Swap control-command key')))); + } + + // Relative mouse mode (gaming mode). + // Only show when server supports MOUSE_TYPE_MOVE_RELATIVE (version >= 1.4.5) + // Note: This feature is only available in Flutter client. Sciter client does not support this. + // Web client is not supported yet due to Pointer Lock API integration complexity with Flutter's input system. + // Wayland is not supported due to cursor warping limitations. + // Mobile: This option is now in GestureHelp widget, shown only when joystick is visible. + final isWayland = isDesktop && isLinux && bind.mainCurrentIsWayland(); + if (isDesktop && + isDefaultConn && + !isWeb && + !isWayland && + ffiModel.keyboard && + !ffiModel.viewOnly && + ffi.inputModel.isRelativeMouseModeSupported) { + v.add(TToggleMenu( + value: ffi.inputModel.relativeMouseMode.value, + onChanged: (value) { + if (value == null) return; + final previousValue = ffi.inputModel.relativeMouseMode.value; + final success = ffi.inputModel.setRelativeMouseMode(value); + if (!success) { + // Revert the observable toggle to reflect the actual state + ffi.inputModel.relativeMouseMode.value = previousValue; + } + }, + child: Text(translate('Relative mouse mode')))); + } + + // reverse mouse wheel + if (ffiModel.keyboard) { + var optionValue = + bind.sessionGetReverseMouseWheelSync(sessionId: sessionId) ?? ''; + if (optionValue == '') { + optionValue = bind.mainGetUserDefaultOption(key: kKeyReverseMouseWheel); + } + onChanged(bool? value) async { + if (value == null) return; + await bind.sessionSetReverseMouseWheel( + sessionId: sessionId, value: value ? 'Y' : 'N'); + } + + final enabled = !ffi.ffiModel.viewOnly; + v.add(TToggleMenu( + value: optionValue == 'Y', + onChanged: enabled ? onChanged : null, + child: Text(translate('Reverse mouse wheel')))); + } + + // swap left right mouse + if (ffiModel.keyboard) { + final option = 'swap-left-right-mouse'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + onChanged(bool? value) { + if (value == null) return; + bind.sessionToggleOption(sessionId: sessionId, value: option); + } + + final enabled = !ffi.ffiModel.viewOnly; + v.add(TToggleMenu( + value: value, + onChanged: enabled ? onChanged : null, + child: Text(translate('swap-left-right-mouse')))); + } + return v; +} + +bool showVirtualDisplayMenu(FFI ffi) { + if (ffi.ffiModel.pi.platform != kPeerPlatformWindows) { + return false; + } + if (!ffi.ffiModel.pi.isInstalled) { + return false; + } + if (ffi.ffiModel.pi.isRustDeskIdd || ffi.ffiModel.pi.isAmyuniIdd) { + return true; + } + return false; +} + +List getVirtualDisplayMenuChildren( + FFI ffi, String id, VoidCallback? clickCallBack) { + if (!showVirtualDisplayMenu(ffi)) { + return []; + } + final pi = ffi.ffiModel.pi; + final privacyModeState = PrivacyModeState.find(id); + if (pi.isRustDeskIdd) { + final virtualDisplays = ffi.ffiModel.pi.RustDeskVirtualDisplays; + final children = []; + for (var i = 0; i < kMaxVirtualDisplayCount; i++) { + children.add(Obx(() => CkbMenuButton( + value: virtualDisplays.contains(i + 1), + onChanged: privacyModeState.isNotEmpty + ? null + : (bool? value) async { + if (value != null) { + bind.sessionToggleVirtualDisplay( + sessionId: ffi.sessionId, index: i + 1, on: value); + clickCallBack?.call(); + } + }, + child: Text('${translate('Virtual display')} ${i + 1}'), + ffi: ffi, + ))); + } + children.add(Divider()); + children.add(Obx(() => MenuButton( + onPressed: privacyModeState.isNotEmpty + ? null + : () { + bind.sessionToggleVirtualDisplay( + sessionId: ffi.sessionId, + index: kAllVirtualDisplay, + on: false); + clickCallBack?.call(); + }, + ffi: ffi, + child: Text(translate('Plug out all')), + ))); + return children; + } + if (pi.isAmyuniIdd) { + final count = ffi.ffiModel.pi.amyuniVirtualDisplayCount; + final children = [ + Obx(() => Row( + children: [ + TextButton( + onPressed: privacyModeState.isNotEmpty || count == 0 + ? null + : () { + bind.sessionToggleVirtualDisplay( + sessionId: ffi.sessionId, index: 0, on: false); + clickCallBack?.call(); + }, + child: Icon(Icons.remove), + ), + Text(count.toString()), + TextButton( + onPressed: privacyModeState.isNotEmpty || count == 4 + ? null + : () { + bind.sessionToggleVirtualDisplay( + sessionId: ffi.sessionId, index: 0, on: true); + clickCallBack?.call(); + }, + child: Icon(Icons.add), + ), + ], + )), + Divider(), + Obx(() => MenuButton( + onPressed: privacyModeState.isNotEmpty || count == 0 + ? null + : () { + bind.sessionToggleVirtualDisplay( + sessionId: ffi.sessionId, + index: kAllVirtualDisplay, + on: false); + clickCallBack?.call(); + }, + ffi: ffi, + child: Text(translate('Plug out all')), + )), + ]; + return children; + } + return []; +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/consts.dart b/shelled/rustdesk-as-ref/flutter/lib/consts.dart new file mode 100644 index 0000000..b1112dd --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/consts.dart @@ -0,0 +1,685 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:get/get.dart'; + +const int kMaxVirtualDisplayCount = 4; +const int kAllVirtualDisplay = -1; + +const double kDesktopRemoteTabBarHeight = 28.0; +const int kInvalidWindowId = -1; +const int kMainWindowId = 0; + +const kAllDisplayValue = -1; + +const kKeyLegacyMode = 'legacy'; +const kKeyMapMode = 'map'; +const kKeyTranslateMode = 'translate'; + +const String kPlatformAdditionsIsWayland = "is_wayland"; +const String kPlatformAdditionsHeadless = "headless"; +const String kPlatformAdditionsIsInstalled = "is_installed"; +const String kPlatformAdditionsIddImpl = "idd_impl"; +const String kPlatformAdditionsRustDeskVirtualDisplays = + "rustdesk_virtual_displays"; +const String kPlatformAdditionsAmyuniVirtualDisplays = + "amyuni_virtual_displays"; +const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard"; +const String kPlatformAdditionsSupportedPrivacyModeImpl = + "supported_privacy_mode_impl"; + +const String kPeerPlatformWindows = "Windows"; +const String kPeerPlatformLinux = "Linux"; +const String kPeerPlatformMacOS = "Mac OS"; +const String kPeerPlatformAndroid = "Android"; +const String kPeerPlatformWebDesktop = "WebDesktop"; + +const double kScrollbarThickness = 12.0; + +/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page" +const String kAppTypeMain = "main"; + +/// [kAppTypeConnectionManager] only for 'Desktop CM Page' +const String kAppTypeConnectionManager = "cm"; + +const String kAppTypeDesktopRemote = "remote"; +const String kAppTypeDesktopFileTransfer = "file transfer"; +const String kAppTypeDesktopViewCamera = "view camera"; +const String kAppTypeDesktopPortForward = "port forward"; +const String kAppTypeDesktopTerminal = "terminal"; + +const String kWindowMainWindowOnTop = "main_window_on_top"; +const String kWindowRefreshCurrentUser = "refresh_current_user"; +const String kWindowGetWindowInfo = "get_window_info"; +const String kWindowGetScreenList = "get_screen_list"; +// This method is not used, maybe it can be removed. +const String kWindowDisableGrabKeyboard = "disable_grab_keyboard"; +const String kWindowActionRebuild = "rebuild"; +const String kWindowEventHide = "hide"; +const String kWindowEventShow = "show"; +const String kWindowConnect = "connect"; +const String kWindowBumpMouse = "bump_mouse"; + +const String kWindowEventNewRemoteDesktop = "new_remote_desktop"; +const String kWindowEventNewFileTransfer = "new_file_transfer"; +const String kWindowEventNewViewCamera = "new_view_camera"; +const String kWindowEventNewPortForward = "new_port_forward"; +const String kWindowEventNewTerminal = "new_terminal"; +const String kWindowEventRestoreTerminalSessions = "restore_terminal_sessions"; +const String kWindowEventActiveSession = "active_session"; +const String kWindowEventActiveDisplaySession = "active_display_session"; +const String kWindowEventGetRemoteList = "get_remote_list"; +const String kWindowEventGetSessionIdList = "get_session_id_list"; +const String kWindowEventRemoteWindowCoords = "remote_window_coords"; +const String kWindowEventSetFullscreen = "set_fullscreen"; + +const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window"; +const String kWindowEventGetCachedSessionData = "get_cached_session_data"; +const String kWindowEventOpenMonitorSession = "open_monitor_session"; + +const String kOptionViewStyle = "view_style"; +const String kOptionScrollStyle = "scroll_style"; +const String kOptionEdgeScrollEdgeThickness = "edge-scroll-edge-thickness"; +const String kOptionImageQuality = "image_quality"; +const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; +const String kOptionTextureRender = "use-texture-render"; +const String kOptionD3DRender = "allow-d3d-render"; +const String kOptionOpenInTabs = "allow-open-in-tabs"; +const String kOptionOpenInWindows = "allow-open-in-windows"; +const String kOptionForceAlwaysRelay = "force-always-relay"; +const String kOptionViewOnly = "view_only"; +const String kOptionEnableLanDiscovery = "enable-lan-discovery"; +const String kOptionWhitelist = "whitelist"; +const String kOptionEnableAbr = "enable-abr"; +const String kOptionEnableRecordSession = "enable-record-session"; +const String kOptionDirectServer = "direct-server"; +const String kOptionDirectAccessPort = "direct-access-port"; +const String kOptionAllowAutoDisconnect = "allow-auto-disconnect"; +const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout"; +const String kOptionEnableHwcodec = "enable-hwcodec"; +const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming"; +const String kOptionAllowAutoRecordOutgoing = "allow-auto-record-outgoing"; +const String kOptionVideoSaveDirectory = "video-save-directory"; +const String kOptionAccessMode = "access-mode"; +const String kOptionEnableKeyboard = "enable-keyboard"; +// "Settings -> Security -> Permissions" +const String kOptionEnableRemotePrinter = "enable-remote-printer"; +const String kOptionEnableClipboard = "enable-clipboard"; +const String kOptionEnableFileTransfer = "enable-file-transfer"; +const String kOptionEnableAudio = "enable-audio"; +const String kOptionEnableCamera = "enable-camera"; +const String kOptionEnableTerminal = "enable-terminal"; +const String kOptionTerminalPersistent = "terminal-persistent"; +const String kOptionEnableTunnel = "enable-tunnel"; +const String kOptionEnableRemoteRestart = "enable-remote-restart"; +const String kOptionEnableBlockInput = "enable-block-input"; +const String kOptionAllowRemoteConfigModification = + "allow-remote-config-modification"; +const String kOptionVerificationMethod = "verification-method"; +const String kOptionApproveMode = "approve-mode"; +const String kOptionAllowNumericOneTimePassword = + "allow-numeric-one-time-password"; +const String kOptionCollapseToolbar = "collapse_toolbar"; +const String kOptionHideToolbar = "hide-toolbar"; +const String kOptionShowRemoteCursor = "show_remote_cursor"; +const String kOptionFollowRemoteCursor = "follow_remote_cursor"; +const String kOptionFollowRemoteWindow = "follow_remote_window"; +const String kOptionZoomCursor = "zoom-cursor"; +const String kOptionShowQualityMonitor = "show_quality_monitor"; +const String kOptionDisableAudio = "disable_audio"; +const String kOptionEnableFileCopyPaste = "enable-file-copy-paste"; +// "Settings -> Display -> Other default options" +const String kOptionDisableClipboard = "disable_clipboard"; +const String kOptionLockAfterSessionEnd = "lock_after_session_end"; +const String kOptionPrivacyMode = "privacy_mode"; +const String kOptionTouchMode = "touch-mode"; +const String kOptionI444 = "i444"; +const String kOptionSwapLeftRightMouse = "swap-left-right-mouse"; +const String kOptionCodecPreference = "codec-preference"; +const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left"; +const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right"; +const String kOptionHideAbTagsPanel = "hideAbTagsPanel"; +const String kOptionRemoteMenubarState = "remoteMenubarState"; +const String kOptionPeerSorting = "peer-sorting"; +const String kOptionPeerTabIndex = "peer-tab-index"; +const String kOptionPeerTabOrder = "peer-tab-order"; +const String kOptionPeerTabVisible = "peer-tab-visible"; +const String kOptionPeerCardUiType = "peer-card-ui-type"; +const String kOptionCurrentAbName = "current-ab-name"; +const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs"; +const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render"; +const String kOptionEnableCheckUpdate = "enable-check-update"; +const String kOptionAllowAutoUpdate = "allow-auto-update"; +const String kOptionAllowLinuxHeadless = "allow-linux-headless"; +const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper"; +const String kOptionStopService = "stop-service"; +const String kOptionDirectxCapture = "enable-directx-capture"; +const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification"; +const String kOptionEnableUdpPunch = "enable-udp-punch"; +const String kOptionEnableIpv6Punch = "enable-ipv6-punch"; +const String kOptionEnableTrustedDevices = "enable-trusted-devices"; +const String kOptionShowVirtualMouse = "show-virtual-mouse"; +const String kOptionVirtualMouseScale = "virtual-mouse-scale"; +const String kOptionShowVirtualJoystick = "show-virtual-joystick"; +const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note"; +const String kOptionEnableShowTerminalExtraKeys = "enable-show-terminal-extra-keys"; + +// network options +const String kOptionAllowWebSocket = "allow-websocket"; +const String kOptionAllowInsecureTLSFallback = "allow-insecure-tls-fallback"; +const String kOptionDisableUdp = "disable-udp"; +const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust"; + +// builtin options +const String kOptionHideServerSetting = "hide-server-settings"; +const String kOptionHideProxySetting = "hide-proxy-settings"; +const String kOptionHideWebSocketSetting = "hide-websocket-settings"; +const String kOptionHideStopService = "hide-stop-service"; +const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings"; +const String kOptionHideSecuritySetting = "hide-security-settings"; +const String kOptionHideNetworkSetting = "hide-network-settings"; +const String kOptionRemovePresetPasswordWarning = + "remove-preset-password-warning"; +const String kOptionDisableChangePermanentPassword = + "disable-change-permanent-password"; +const String kOptionDisableChangeId = "disable-change-id"; +const String kOptionDisableUnlockPin = "disable-unlock-pin"; +const kHideUsernameOnCard = "hide-username-on-card"; +const String kOptionHideHelpCards = "hide-help-cards"; + +const String kOptionToggleViewOnly = "view-only"; +const String kOptionToggleShowMyCursor = "show-my-cursor"; + +const String kOptionDisableFloatingWindow = "disable-floating-window"; + +const String kOptionKeepScreenOn = "keep-screen-on"; + +const String kOptionKeepAwakeDuringIncomingSessions = "keep-awake-during-incoming-sessions"; +const String kOptionKeepAwakeDuringOutgoingSessions = "keep-awake-during-outgoing-sessions"; + +const String kOptionShowMobileAction = "showMobileActions"; + +const String kUrlActionClose = "close"; + +const String kTabLabelHomePage = "Home"; +const String kTabLabelSettingPage = "Settings"; + +const String kWindowPrefix = "wm_"; +const int kWindowMainId = 0; + +const String kPointerEventKindTouch = "touch"; +const String kPointerEventKindMouse = "mouse"; + +const String kMouseEventTypeDefault = ""; +const String kMouseEventTypePanStart = "pan_start"; +const String kMouseEventTypePanUpdate = "pan_update"; +const String kMouseEventTypePanEnd = "pan_end"; +const String kMouseEventTypeDown = "down"; +const String kMouseEventTypeUp = "up"; + +const String kKeyFlutterKey = "flutter_key"; + +const String kKeyShowDisplaysAsIndividualWindows = + 'displays_as_individual_windows'; +const String kKeyUseAllMyDisplaysForTheRemoteSession = + 'use_all_my_displays_for_the_remote_session'; +const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar'; +const String kKeyReverseMouseWheel = "reverse_mouse_wheel"; + +const String kMsgboxTextWaitingForImage = 'Connected, waiting for image...'; + +// the executable name of the portable version +const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; + +const Color kColorWarn = Color.fromARGB(255, 245, 133, 59); +const Color kColorCanvas = Colors.black; + +const int kMobileDefaultDisplayWidth = 720; +const int kMobileDefaultDisplayHeight = 1280; + +const int kDesktopDefaultDisplayWidth = 1080; +const int kDesktopDefaultDisplayHeight = 720; + +const int kMobileMaxDisplaySize = 1280; +const int kDesktopMaxDisplaySize = 3840; + +const double kDesktopFileTransferRowHeight = 30.0; +const double kDesktopFileTransferHeaderHeight = 25.0; + +const double kMinFps = 5; +const double kDefaultFps = 30; +const double kMaxFps = 120; + +const double kMinQuality = 10; +const double kDefaultQuality = 50; +const double kMaxQuality = 100; +const double kMaxMoreQuality = 2000; + +// trackpad speed +const String kKeyTrackpadSpeed = 'trackpad-speed'; +const int kMinTrackpadSpeed = 10; +const int kDefaultTrackpadSpeed = 100; +const int kMaxTrackpadSpeed = 1000; + +// relative mouse mode +/// Throttle duration (in milliseconds) for updating pointer lock center during +/// window move/resize events. Lower values provide more responsive updates but +/// may cause performance issues during rapid window operations. +const int kDefaultPointerLockCenterThrottleMs = 100; + +/// Minimum server version required for relative mouse mode (MOUSE_TYPE_MOVE_RELATIVE). +/// Servers older than this version will ignore relative mouse events. +/// +/// IMPORTANT: This value must be kept in sync with the Rust constant +/// `MIN_VERSION_RELATIVE_MOUSE_MODE` in `src/common.rs`. +const String kMinVersionForRelativeMouseMode = '1.4.5'; + +/// Maximum delta value for relative mouse movement. +/// Large values could cause issues with i32 overflow on server side, +/// and no reasonable mouse movement should exceed this bound. +/// +/// IMPORTANT: This value must be kept in sync with the Rust constant +/// `MAX_RELATIVE_MOUSE_DELTA` in `src/server/input_service.rs`. +const int kMaxRelativeMouseDelta = 10000; + +/// Debounce duration (in milliseconds) for relative mouse mode toggle. +/// This prevents double-toggle from race condition between Rust rdev grab loop +/// and Flutter keyboard handling. Value should be small enough to allow +/// intentional quick toggles but large enough to prevent accidental double-triggers. +const int kRelativeMouseModeToggleDebounceMs = 150; + +// incomming (should be incoming) is kept, because change it will break the previous setting. +const String kKeyPrinterIncomingJobAction = 'printer-incomming-job-action'; +const String kValuePrinterIncomingJobDismiss = 'dismiss'; +const String kValuePrinterIncomingJobDefault = ''; +const String kValuePrinterIncomingJobSelected = 'selected'; +const String kKeyPrinterSelected = 'printer-selected-name'; +const String kKeyPrinterSave = 'allow-printer-dialog-save'; +const String kKeyPrinterAllowAutoPrint = 'allow-printer-auto-print'; + +double kNewWindowOffset = isWindows + ? 56.0 + : isLinux + ? 50.0 + : isMacOS + ? 30.0 + : 50.0; + +EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux + ? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value + ? EdgeInsets.zero + : EdgeInsets.all(5.0) + : EdgeInsets.zero; +// https://en.wikipedia.org/wiki/Non-breaking_space +const int $nbsp = 0x00A0; + +extension StringExtension on String { + String get nonBreaking => replaceAll(' ', String.fromCharCode($nbsp)); +} + +const Size kConnectionManagerWindowSizeClosedChat = Size(300, 490); +const Size kConnectionManagerWindowSizeOpenChat = Size(700, 490); +// Tabbar transition duration, now we remove the duration +const Duration kTabTransitionDuration = Duration.zero; +const double kEmptyMarginTop = 50; +const double kDesktopIconButtonSplashRadius = 20; + +/// [kMinCursorSize] indicates min cursor (w, h) +const int kMinCursorSize = 12; + +const kFullScreenEdgeSize = 0.0; +const kMaximizeEdgeSize = 0.0; +// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead. +const kWindowResizeEdgeSize = 5.0; +final kWindowBorderWidth = isWindows ? 0.0 : 1.0; +const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); +const kFrameBorderRadius = 12.0; +const kFrameClipRRectBorderRadius = 12.0; +const kFrameBoxShadowBlurRadius = 32.0; +const kFrameBoxShadowOffsetFocused = 4.0; +const kFrameBoxShadowOffsetUnfocused = 2.0; + +const kInvalidValueStr = 'InvalidValueStr'; + +// Config key shared by flutter and other ui. +const kCommConfKeyTheme = 'theme'; +const kCommConfKeyLang = 'lang'; + +const kMobilePageConstraints = BoxConstraints(maxWidth: 600); + +/// [kMouseControlDistance] indicates the distance that self-side move to get control of mouse. +const kMouseControlDistance = 12; + +/// [kMouseControlTimeoutMSec] indicates the timeout (in milliseconds) that self-side can get control of mouse. +const kMouseControlTimeoutMSec = 1000; + +/// [kRemoteViewStyleOriginal] Show remote image without scaling. +const kRemoteViewStyleOriginal = 'original'; + +/// [kRemoteViewStyleAdaptive] Show remote image scaling by ratio factor. +const kRemoteViewStyleAdaptive = 'adaptive'; + +/// [kRemoteViewStyleCustom] Show remote image at a user-defined scale percent. +const kRemoteViewStyleCustom = 'custom'; + +/// [kRemoteScrollStyleAuto] Scroll image auto by position. +const kRemoteScrollStyleAuto = 'scrollauto'; + +/// [kRemoteScrollStyleBar] Scroll image with scroll bar. +const kRemoteScrollStyleBar = 'scrollbar'; + +/// [kRemoteScrollStyleEdge] Scroll image auto at edges. +const kRemoteScrollStyleEdge = 'scrolledge'; + +/// [kScrollModeDefault] Mouse or touchpad, the default scroll mode. +const kScrollModeDefault = 'default'; + +/// [kScrollModeReverse] Mouse or touchpad, the reverse scroll mode. +const kScrollModeReverse = 'reverse'; + +/// [kRemoteImageQualityBest] Best image quality. +const kRemoteImageQualityBest = 'best'; + +/// [kRemoteImageQualityBalanced] Balanced image quality, mid performance. +const kRemoteImageQualityBalanced = 'balanced'; + +/// [kRemoteImageQualityLow] Low image quality, better performance. +const kRemoteImageQualityLow = 'low'; + +/// [kRemoteImageQualityCustom] Custom image quality. +const kRemoteImageQualityCustom = 'custom'; + +const kIgnoreDpi = true; + +const Set kTouchBasedDeviceKinds = { + PointerDeviceKind.touch, + PointerDeviceKind.stylus, + PointerDeviceKind.invertedStylus, +}; + +// Scale custom related constants +const String kCustomScalePercentKey = + 'custom_scale_percent'; // Flutter option key for storing custom scale percent (integer 5-1000) +const int kScaleCustomMinPercent = 5; +const int kScaleCustomPivotPercent = 100; // 100% should be at 1/3 of track +const int kScaleCustomMaxPercent = 1000; +const double kScaleCustomPivotPos = 1.0 / 3.0; // first 1/3 → up to 100% +const double kScaleCustomDetentEpsilon = + 0.006; // snap range around pivot (~0.6%) +const Duration kDebounceCustomScaleDuration = Duration(milliseconds: 300); + +// ================================ mobile ================================ + +// Magic numbers, maybe need to avoid it or use a better way to get them. +const kMobileDelaySoftKeyboard = Duration(milliseconds: 30); +const kMobileDelaySoftKeyboardFocus = Duration(milliseconds: 30); + +/// Android constants +const kActionApplicationDetailsSettings = + "android.settings.APPLICATION_DETAILS_SETTINGS"; +const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS"; + +const kRecordAudio = "android.permission.RECORD_AUDIO"; +const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE"; +const kRequestIgnoreBatteryOptimizations = + "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"; +const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW"; +const kAndroid13Notification = "android.permission.POST_NOTIFICATIONS"; + +/// Android channel invoke type key +class AndroidChannel { + static final kStartAction = "start_action"; + static final kGetStartOnBootOpt = "get_start_on_boot_opt"; + static final kSetStartOnBootOpt = "set_start_on_boot_opt"; + static final kSyncAppDirConfigPath = "sync_app_dir"; +} + +/// 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_NUMPAD1', + 0x0007005a: 'VK_NUMPAD2', + 0x0007005b: 'VK_NUMPAD3', + 0x0007005c: 'VK_NUMPAD4', + 0x0007005d: 'VK_NUMPAD5', + 0x0007005e: 'VK_NUMPAD6', + 0x0007005f: 'VK_NUMPAD7', + 0x00070060: 'VK_NUMPAD8', + 0x00070061: 'VK_NUMPAD9', + 0x00070062: 'VK_NUMPAD0', + 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', +}; + +/// The windows targets in the publish time order. +enum WindowsTarget { + naw, // not a windows target + xp, + vista, + w7, + w8, + w8_1, + w10, + w11 +} + +/// A convenient method to transform a build number to the corresponding windows version. +extension WindowsTargetExt on int { + WindowsTarget get windowsVersion => getWindowsTarget(this); +} + +const kCheckSoftwareUpdateFinish = 'check_software_update_finish'; diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/connection_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/connection_page.dart new file mode 100644 index 0000000..bdf3829 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/connection_page.dart @@ -0,0 +1,615 @@ +// main window right pane + +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/connection_page_title.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:window_manager/window_manager.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; + +import '../../common.dart'; +import '../../common/formatter/id_formatter.dart'; +import '../../common/widgets/peer_tab_page.dart'; +import '../../common/widgets/autocomplete.dart'; +import '../../models/platform_model.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; + +class OnlineStatusWidget extends StatefulWidget { + const OnlineStatusWidget({Key? key, this.onSvcStatusChanged}) + : super(key: key); + + final VoidCallback? onSvcStatusChanged; + + @override + State createState() => _OnlineStatusWidgetState(); +} + +/// State for the connection page. +class _OnlineStatusWidgetState extends State { + final _svcStopped = Get.find(tag: 'stop-service'); + final _svcIsUsingPublicServer = true.obs; + Timer? _updateTimer; + + double get em => 14.0; + double? get height => bind.isIncomingOnly() ? null : em * 3; + + void onUsePublicServerGuide() { + const url = "https://rustdesk.com/pricing"; + canLaunchUrlString(url).then((can) { + if (can) { + launchUrlString(url); + } + }); + } + + @override + void initState() { + super.initState(); + _updateTimer = periodic_immediate(Duration(seconds: 1), () async { + updateStatus(); + }); + } + + @override + void dispose() { + _updateTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isIncomingOnly = bind.isIncomingOnly(); + startServiceWidget() => Offstage( + offstage: !_svcStopped.value, + child: InkWell( + onTap: () async { + await start_service(true); + }, + child: Text(translate("Start service"), + style: TextStyle( + decoration: TextDecoration.underline, fontSize: em))) + .marginOnly(left: em), + ); + + setupServerWidget() => Flexible( + child: Offstage( + offstage: !(!_svcStopped.value && + stateGlobal.svcStatus.value == SvcStatus.ready && + _svcIsUsingPublicServer.value), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(', ', style: TextStyle(fontSize: em)), + Flexible( + child: InkWell( + onTap: onUsePublicServerGuide, + child: Row( + children: [ + Flexible( + child: Text( + translate('setup_server_tip'), + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: em), + ), + ), + ], + ), + ), + ) + ], + ), + ), + ); + + basicWidget() => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 8, + width: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: _svcStopped.value || + stateGlobal.svcStatus.value == SvcStatus.connecting + ? kColorWarn + : (stateGlobal.svcStatus.value == SvcStatus.ready + ? Color.fromARGB(255, 50, 190, 166) + : Color.fromARGB(255, 224, 79, 95)), + ), + ).marginSymmetric(horizontal: em), + Container( + width: isIncomingOnly ? 226 : null, + child: _buildConnStatusMsg(), + ), + // stop + if (!isIncomingOnly) startServiceWidget(), + // ready && public + // No need to show the guide if is custom client. + if (!isIncomingOnly) setupServerWidget(), + ], + ); + + return Container( + height: height, + child: Obx(() => isIncomingOnly + ? Column( + children: [ + basicWidget(), + Align( + child: startServiceWidget(), + alignment: Alignment.centerLeft) + .marginOnly(top: 2.0, left: 22.0), + ], + ) + : basicWidget()), + ).paddingOnly(right: isIncomingOnly ? 8 : 0); + } + + _buildConnStatusMsg() { + widget.onSvcStatusChanged?.call(); + return Text( + _svcStopped.value + ? translate("Service is not running") + : stateGlobal.svcStatus.value == SvcStatus.connecting + ? translate("connecting_status") + : stateGlobal.svcStatus.value == SvcStatus.notReady + ? translate("not_ready_status") + : translate('Ready'), + style: TextStyle(fontSize: em), + ); + } + + updateStatus() async { + final status = + jsonDecode(await bind.mainGetConnectStatus()) as Map; + final statusNum = status['status_num'] as int; + if (statusNum == 0) { + stateGlobal.svcStatus.value = SvcStatus.connecting; + } else if (statusNum == -1) { + stateGlobal.svcStatus.value = SvcStatus.notReady; + } else if (statusNum == 1) { + stateGlobal.svcStatus.value = SvcStatus.ready; + } else { + stateGlobal.svcStatus.value = SvcStatus.notReady; + } + _svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer(); + try { + stateGlobal.videoConnCount.value = status['video_conn_count'] as int; + } catch (_) {} + } +} + +/// Connection page for connecting to a remote peer. +class ConnectionPage extends StatefulWidget { + const ConnectionPage({Key? key}) : super(key: key); + + @override + State createState() => _ConnectionPageState(); +} + +/// State for the connection page. +class _ConnectionPageState extends State + with SingleTickerProviderStateMixin, WindowListener { + /// Controller for the id input bar. + final _idController = IDTextEditingController(); + + final RxBool _idInputFocused = false.obs; + final FocusNode _idFocusNode = FocusNode(); + final TextEditingController _idEditingController = TextEditingController(); + + String selectedConnectionType = 'Connect'; + + bool isWindowMinimized = false; + + final AllPeersLoader _allPeersLoader = AllPeersLoader(); + + // https://github.com/flutter/flutter/issues/157244 + Iterable _autocompleteOpts = []; + + final _menuOpen = false.obs; + + @override + void initState() { + super.initState(); + _allPeersLoader.init(setState); + _idFocusNode.addListener(onFocusChanged); + if (_idController.text.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final lastRemoteId = await bind.mainGetLastRemoteId(); + if (lastRemoteId != _idController.id) { + setState(() { + _idController.id = lastRemoteId; + }); + } + }); + } + Get.put(_idEditingController); + Get.put(_idController); + windowManager.addListener(this); + } + + @override + void dispose() { + _idController.dispose(); + windowManager.removeListener(this); + _allPeersLoader.clear(); + _idFocusNode.removeListener(onFocusChanged); + _idFocusNode.dispose(); + _idEditingController.dispose(); + if (Get.isRegistered()) { + Get.delete(); + } + if (Get.isRegistered()) { + Get.delete(); + } + super.dispose(); + } + + @override + void onWindowEvent(String eventName) { + super.onWindowEvent(eventName); + if (eventName == 'minimize') { + isWindowMinimized = true; + } else if (eventName == 'maximize' || eventName == 'restore') { + if (isWindowMinimized && isWindows) { + // windows can't update when minimized. + Get.forceAppUpdate(); + } + isWindowMinimized = false; + } + } + + @override + void onWindowEnterFullScreen() { + // Remove edge border by setting the value to zero. + stateGlobal.resizeEdgeSize.value = 0; + } + + @override + void onWindowLeaveFullScreen() { + // Restore edge border to default edge size. + stateGlobal.resizeEdgeSize.value = stateGlobal.isMaximized.isTrue + ? kMaximizeEdgeSize + : windowResizeEdgeSize; + } + + @override + void onWindowClose() { + super.onWindowClose(); + bind.mainOnMainWindowClose(); + } + + void onFocusChanged() { + _idInputFocused.value = _idFocusNode.hasFocus; + if (_idFocusNode.hasFocus) { + if (_allPeersLoader.needLoad) { + _allPeersLoader.getAllPeers(); + } + + final textLength = _idEditingController.value.text.length; + // Select all to facilitate removing text, just following the behavior of address input of chrome. + _idEditingController.selection = + TextSelection(baseOffset: 0, extentOffset: textLength); + } + } + + @override + Widget build(BuildContext context) { + final isOutgoingOnly = bind.isOutgoingOnly(); + return Column( + children: [ + Expanded( + child: Column( + children: [ + Row( + children: [ + Flexible(child: _buildRemoteIDTextField(context)), + ], + ).marginOnly(top: 22), + SizedBox(height: 12), + Divider().paddingOnly(right: 12), + Expanded(child: PeerTabPage()), + ], + ).paddingOnly(left: 12.0)), + if (!isOutgoingOnly) const Divider(height: 1), + if (!isOutgoingOnly) OnlineStatusWidget() + ], + ); + } + + /// Callback for the connect button. + /// Connects to the selected peer. + void onConnect( + {bool isFileTransfer = false, + bool isViewCamera = false, + bool isTerminal = false}) { + var id = _idController.id; + connect(context, id, + isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, + isTerminal: isTerminal); + } + + /// UI for the remote ID TextField. + /// Search for a peer. + Widget _buildRemoteIDTextField(BuildContext context) { + var w = Container( + width: 320 + 20 * 2, + padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(13)), + border: Border.all(color: Theme.of(context).colorScheme.background)), + child: Ink( + child: Column( + children: [ + getConnectionPageTitle(context, false).marginOnly(bottom: 15), + Row( + children: [ + Expanded( + child: RawAutocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text == '') { + _autocompleteOpts = const Iterable.empty(); + } else if (_allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded) { + Peer emptyPeer = Peer( + id: '', + username: '', + hostname: '', + alias: '', + platform: '', + tags: [], + hash: '', + password: '', + forceAlwaysRelay: false, + rdpPort: '', + rdpUsername: '', + loginName: '', + device_group_name: '', + note: '', + ); + _autocompleteOpts = [emptyPeer]; + } else { + String textWithoutSpaces = + textEditingValue.text.replaceAll(" ", ""); + if (int.tryParse(textWithoutSpaces) != null) { + textEditingValue = TextEditingValue( + text: textWithoutSpaces, + selection: textEditingValue.selection, + ); + } + String textToFind = textEditingValue.text.toLowerCase(); + _autocompleteOpts = _allPeersLoader.peers + .where((peer) => + peer.id.toLowerCase().contains(textToFind) || + peer.username + .toLowerCase() + .contains(textToFind) || + peer.hostname + .toLowerCase() + .contains(textToFind) || + peer.alias.toLowerCase().contains(textToFind)) + .toList(); + } + return _autocompleteOpts; + }, + focusNode: _idFocusNode, + textEditingController: _idEditingController, + fieldViewBuilder: ( + BuildContext context, + TextEditingController fieldTextEditingController, + FocusNode fieldFocusNode, + VoidCallback onFieldSubmitted, + ) { + updateTextAndPreserveSelection( + fieldTextEditingController, _idController.text); + return Obx(() => TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + focusNode: fieldFocusNode, + style: const TextStyle( + fontFamily: 'WorkSans', + fontSize: 22, + height: 1.4, + ), + maxLines: 1, + cursorColor: + Theme.of(context).textTheme.titleLarge?.color, + decoration: InputDecoration( + filled: false, + counterText: '', + hintText: _idInputFocused.value + ? null + : translate('Enter Remote ID'), + contentPadding: const EdgeInsets.symmetric( + horizontal: 15, vertical: 13)), + controller: fieldTextEditingController, + inputFormatters: [IDTextInputFormatter()], + onChanged: (v) { + _idController.id = v; + }, + onSubmitted: (_) { + onConnect(); + }, + ).workaroundFreezeLinuxMint()); + }, + onSelected: (option) { + setState(() { + _idController.id = option.id; + FocusScope.of(context).unfocus(); + }); + }, + optionsViewBuilder: (BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options) { + options = _autocompleteOpts; + double maxHeight = options.length * 50; + if (options.length == 1) { + maxHeight = 52; + } else if (options.length == 3) { + maxHeight = 146; + } else if (options.length == 4) { + maxHeight = 193; + } + maxHeight = maxHeight.clamp(0, 200); + + return Align( + alignment: Alignment.topLeft, + child: Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 5, + spreadRadius: 1, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Material( + elevation: 4, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: maxHeight, + maxWidth: 319, + ), + child: _allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded + ? Container( + height: 80, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + ), + )) + : Padding( + padding: + const EdgeInsets.only(top: 5), + child: ListView( + children: options + .map((peer) => + AutocompletePeerTile( + onSelect: () => + onSelected(peer), + peer: peer)) + .toList(), + ), + ), + ), + ))), + ); + }, + )), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 13.0), + child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + SizedBox( + height: 28.0, + child: ElevatedButton( + onPressed: () { + onConnect(); + }, + child: Text(translate("Connect")), + ), + ), + const SizedBox(width: 8), + Container( + height: 28.0, + width: 28.0, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: StatefulBuilder( + builder: (context, setState) { + var offset = Offset(0, 0); + return Obx(() => InkWell( + child: _menuOpen.value + ? Transform.rotate( + angle: pi, + child: Icon(IconFont.more, size: 14), + ) + : Icon(IconFont.more, size: 14), + onTapDown: (e) { + offset = e.globalPosition; + }, + onTap: () async { + _menuOpen.value = true; + final x = offset.dx; + final y = offset.dy; + await mod_menu + .showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: [ + ( + 'Transfer file', + () => onConnect(isFileTransfer: true) + ), + ( + 'View camera', + () => onConnect(isViewCamera: true) + ), + ( + '${translate('Terminal')} (beta)', + () => onConnect(isTerminal: true) + ), + ] + .map((e) => MenuEntryButton( + childBuilder: (TextStyle? style) => + Text( + translate(e.$1), + style: style, + ), + proc: () => e.$2(), + padding: EdgeInsets.symmetric( + horizontal: + kDesktopMenuPadding.left), + dismissOnClicked: true, + )) + .map((e) => e.build( + context, + const MenuConfig( + commonColor: CustomPopupMenuTheme + .commonColor, + height: + CustomPopupMenuTheme.height, + dividerHeight: + CustomPopupMenuTheme + .dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, + ) + .then((_) { + _menuOpen.value = false; + }); + }, + )); + }, + ), + ), + ), + ]), + ), + ], + ), + ), + ); + return Container( + constraints: const BoxConstraints(maxWidth: 600), child: w); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_home_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_home_page.dart new file mode 100644 index 0000000..42ec100 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_home_page.dart @@ -0,0 +1,1146 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:convert'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/animated_rotation_widget.dart'; +import 'package:flutter_hbb/common/widgets/custom_password.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/pages/connection_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/desktop/widgets/update_progress.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/plugin/ui_manager.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:window_manager/window_manager.dart'; +import 'package:window_size/window_size.dart' as window_size; +import '../widgets/button.dart'; + +class DesktopHomePage extends StatefulWidget { + const DesktopHomePage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopHomePageState(); +} + +const borderColor = Color(0xFF2F65BA); + +class _DesktopHomePageState extends State + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { + final _leftPaneScrollController = ScrollController(); + + @override + bool get wantKeepAlive => true; + var systemError = ''; + StreamSubscription? _uniLinksSubscription; + var svcStopped = false.obs; + var watchIsCanScreenRecording = false; + var watchIsProcessTrust = false; + var watchIsInputMonitoring = false; + var watchIsCanRecordAudio = false; + Timer? _updateTimer; + bool isCardClosed = false; + + final RxBool _editHover = false.obs; + final RxBool _block = false.obs; + + final GlobalKey _childKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + super.build(context); + final isIncomingOnly = bind.isIncomingOnly(); + return _buildBlock( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildLeftPane(context), + if (!isIncomingOnly) const VerticalDivider(width: 1), + if (!isIncomingOnly) Expanded(child: buildRightPane(context)), + ], + )); + } + + Widget _buildBlock({required Widget child}) { + return buildRemoteBlock( + block: _block, mask: true, use: canBeBlocked, child: child); + } + + Widget buildLeftPane(BuildContext context) { + final isIncomingOnly = bind.isIncomingOnly(); + final isOutgoingOnly = bind.isOutgoingOnly(); + final children = [ + if (!isOutgoingOnly) buildPresetPasswordWarning(), + if (bind.isCustomClient()) + Align( + alignment: Alignment.center, + child: loadPowered(context), + ), + Align( + alignment: Alignment.center, + child: loadLogo(), + ), + buildTip(context), + if (!isOutgoingOnly) buildIDBoard(context), + if (!isOutgoingOnly) buildPasswordBoard(context), + FutureBuilder( + future: Future.value( + Obx(() => buildHelpCards(stateGlobal.updateUrl.value))), + builder: (_, data) { + if (data.hasData) { + if (isIncomingOnly) { + if (isInHomePage()) { + Future.delayed(Duration(milliseconds: 300), () { + _updateWindowSize(); + }); + } + } + return data.data!; + } else { + return const Offstage(); + } + }, + ), + buildPluginEntry(), + ]; + if (isIncomingOnly) { + children.addAll([ + Divider(), + OnlineStatusWidget( + onSvcStatusChanged: () { + if (isInHomePage()) { + Future.delayed(Duration(milliseconds: 300), () { + _updateWindowSize(); + }); + } + }, + ).marginOnly(bottom: 6, right: 6) + ]); + } + final textColor = Theme.of(context).textTheme.titleLarge?.color; + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Container( + width: isIncomingOnly ? 280.0 : 200.0, + color: Theme.of(context).colorScheme.background, + child: Stack( + children: [ + Column( + children: [ + SingleChildScrollView( + controller: _leftPaneScrollController, + child: Column( + key: _childKey, + children: children, + ), + ), + Expanded(child: Container()) + ], + ), + if (isOutgoingOnly) + Positioned( + bottom: 6, + left: 12, + child: Align( + alignment: Alignment.centerLeft, + child: InkWell( + child: Obx( + () => Icon( + Icons.settings, + color: _editHover.value + ? textColor + : Colors.grey.withOpacity(0.5), + size: 22, + ), + ), + onTap: () => { + if (DesktopSettingPage.tabKeys.isNotEmpty) + { + DesktopSettingPage.switch2page( + DesktopSettingPage.tabKeys[0]) + } + }, + onHover: (value) => _editHover.value = value, + ), + ), + ) + ], + ), + ), + ); + } + + buildRightPane(BuildContext context) { + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: ConnectionPage(), + ); + } + + buildIDBoard(BuildContext context) { + final model = gFFI.serverModel; + return Container( + margin: const EdgeInsets.only(left: 20, right: 11), + height: 57, + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Container( + width: 2, + decoration: const BoxDecoration(color: MyTheme.accent), + ).marginOnly(top: 5), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 7), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 25, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("ID"), + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5)), + ).marginOnly(top: 5), + buildPopupMenu(context) + ], + ), + ), + Flexible( + child: GestureDetector( + onDoubleTap: () { + Clipboard.setData( + ClipboardData(text: model.serverId.text)); + showToast(translate("Copied")); + }, + child: TextFormField( + controller: model.serverId, + readOnly: true, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.only(top: 10, bottom: 10), + ), + style: TextStyle( + fontSize: 22, + ), + ).workaroundFreezeLinuxMint(), + ), + ) + ], + ), + ), + ), + ], + ), + ); + } + + Widget buildPopupMenu(BuildContext context) { + final textColor = Theme.of(context).textTheme.titleLarge?.color; + RxBool hover = false.obs; + return InkWell( + onTap: DesktopTabPage.onAddSetting, + child: Tooltip( + message: translate('Settings'), + child: Obx( + () => CircleAvatar( + radius: 15, + backgroundColor: hover.value + ? Theme.of(context).scaffoldBackgroundColor + : Theme.of(context).colorScheme.background, + child: Icon( + Icons.more_vert_outlined, + size: 20, + color: hover.value ? textColor : textColor?.withOpacity(0.5), + ), + ), + ), + ), + onHover: (value) => hover.value = value, + ); + } + + buildPasswordBoard(BuildContext context) { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer( + builder: (context, model, child) { + return buildPasswordBoard2(context, model); + }, + )); + } + + buildPasswordBoard2(BuildContext context, ServerModel model) { + RxBool refreshHover = false.obs; + RxBool editHover = false.obs; + final textColor = Theme.of(context).textTheme.titleLarge?.color; + final showOneTime = model.approveMode != 'click' && + model.verificationMethod != kUsePermanentPassword; + return Container( + margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13), + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Container( + width: 2, + height: 52, + decoration: BoxDecoration(color: MyTheme.accent), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 7), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText( + translate("One-time Password"), + style: TextStyle( + fontSize: 14, color: textColor?.withOpacity(0.5)), + maxLines: 1, + ), + Row( + children: [ + Expanded( + child: GestureDetector( + onDoubleTap: () { + if (showOneTime) { + Clipboard.setData( + ClipboardData(text: model.serverPasswd.text)); + showToast(translate("Copied")); + } + }, + child: TextFormField( + controller: model.serverPasswd, + readOnly: true, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: + EdgeInsets.only(top: 14, bottom: 10), + ), + style: TextStyle(fontSize: 15), + ).workaroundFreezeLinuxMint(), + ), + ), + if (showOneTime) + AnimatedRotationWidget( + onPressed: () => bind.mainUpdateTemporaryPassword(), + child: Tooltip( + message: translate('Refresh Password'), + child: Obx(() => RotatedBox( + quarterTurns: 2, + child: Icon( + Icons.refresh, + color: refreshHover.value + ? textColor + : Color(0xFFDDDDDD), + size: 22, + ))), + ), + onHover: (value) => refreshHover.value = value, + ).marginOnly(right: 8, top: 4), + if (!bind.isDisableSettings()) + InkWell( + child: Tooltip( + message: translate('Change Password'), + child: Obx( + () => Icon( + Icons.edit, + color: editHover.value + ? textColor + : Color(0xFFDDDDDD), + size: 22, + ).marginOnly(right: 8, top: 4), + ), + ), + onTap: () => DesktopSettingPage.switch2page( + SettingsTabKey.safety), + onHover: (value) => editHover.value = value, + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } + + buildTip(BuildContext context) { + final isOutgoingOnly = bind.isOutgoingOnly(); + return Padding( + padding: + const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + if (!isOutgoingOnly) + Align( + alignment: Alignment.centerLeft, + child: Text( + translate("Your Desktop"), + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ], + ), + SizedBox( + height: 10.0, + ), + if (!isOutgoingOnly) + Text( + translate("desk_tip"), + overflow: TextOverflow.clip, + style: Theme.of(context).textTheme.bodySmall, + ), + if (isOutgoingOnly) + Text( + translate("outgoing_only_desk_tip"), + overflow: TextOverflow.clip, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ); + } + + Widget buildHelpCards(String updateUrl) { + if (!bind.isCustomClient() && + updateUrl.isNotEmpty && + !isCardClosed && + bind.mainUriPrefixSync().contains('rustdesk')) { + final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled(); + String btnText = isToUpdate ? 'Update' : 'Download'; + GestureTapCallback onPressed = () async { + final Uri url = Uri.parse('https://rustdesk.com/download'); + await launchUrl(url); + }; + if (isToUpdate) { + onPressed = () { + handleUpdate(updateUrl); + }; + } + return buildInstallCard( + "Status", + "${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).", + btnText, + onPressed, + closeButton: true, + help: isToUpdate ? 'Changelog' : null, + link: isToUpdate + ? 'https://github.com/rustdesk/rustdesk/releases/tag/${bind.mainGetNewVersion()}' + : null); + } + if (systemError.isNotEmpty) { + return buildInstallCard("", systemError, "", () {}); + } + + if (isWindows && !bind.isDisableInstallation()) { + if (!bind.mainIsInstalled()) { + return buildInstallCard( + "", bind.isOutgoingOnly() ? "" : "install_tip", "Install", + () async { + await rustDeskWinManager.closeAllSubWindows(); + bind.mainGotoInstall(); + }); + } else if (bind.mainIsInstalledLowerVersion()) { + return buildInstallCard( + "Status", "Your installation is lower version.", "Click to upgrade", + () async { + await rustDeskWinManager.closeAllSubWindows(); + bind.mainUpdateMe(); + }); + } + } else if (isMacOS) { + final isOutgoingOnly = bind.isOutgoingOnly(); + if (!(isOutgoingOnly || bind.mainIsCanScreenRecording(prompt: false))) { + return buildInstallCard("Permissions", "config_screen", "Configure", + () async { + bind.mainIsCanScreenRecording(prompt: true); + watchIsCanScreenRecording = true; + }, help: 'Help', link: translate("doc_mac_permission")); + } else if (!isOutgoingOnly && !bind.mainIsProcessTrusted(prompt: false)) { + return buildInstallCard("Permissions", "config_acc", "Configure", + () async { + bind.mainIsProcessTrusted(prompt: true); + watchIsProcessTrust = true; + }, help: 'Help', link: translate("doc_mac_permission")); + } else if (!bind.mainIsCanInputMonitoring(prompt: false)) { + return buildInstallCard("Permissions", "config_input", "Configure", + () async { + bind.mainIsCanInputMonitoring(prompt: true); + watchIsInputMonitoring = true; + }, help: 'Help', link: translate("doc_mac_permission")); + } else if (!isOutgoingOnly && + !svcStopped.value && + bind.mainIsInstalled() && + !bind.mainIsInstalledDaemon(prompt: false)) { + return buildInstallCard("", "install_daemon_tip", "Install", () async { + bind.mainIsInstalledDaemon(prompt: true); + }); + } + //// Disable microphone configuration for macOS. We will request the permission when needed. + // else if ((await osxCanRecordAudio() != + // PermissionAuthorizeType.authorized)) { + // return buildInstallCard("Permissions", "config_microphone", "Configure", + // () async { + // osxRequestAudio(); + // watchIsCanRecordAudio = true; + // }); + // } + } else if (isLinux) { + if (bind.isOutgoingOnly()) { + return Container(); + } + final LinuxCards = []; + if (bind.isSelinuxEnforcing()) { + // Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple. + final keyShowSelinuxHelpTip = "show-selinux-help-tip"; + if (bind.mainGetLocalOption(key: keyShowSelinuxHelpTip) != 'N') { + LinuxCards.add(buildInstallCard( + "Warning", + "selinux_tip", + "", + () async {}, + marginTop: LinuxCards.isEmpty ? 20.0 : 5.0, + help: 'Help', + link: + 'https://rustdesk.com/docs/en/client/linux/#permissions-issue', + closeButton: true, + closeOption: keyShowSelinuxHelpTip, + )); + } + } + if (bind.mainCurrentIsWayland()) { + LinuxCards.add(buildInstallCard( + "Warning", "wayland_experiment_tip", "", () async {}, + marginTop: LinuxCards.isEmpty ? 20.0 : 5.0, + help: 'Help', + link: 'https://rustdesk.com/docs/en/client/linux/#x11-required')); + } else if (bind.mainIsLoginWayland()) { + LinuxCards.add(buildInstallCard("Warning", + "Login screen using Wayland is not supported", "", () async {}, + marginTop: LinuxCards.isEmpty ? 20.0 : 5.0, + help: 'Help', + link: 'https://rustdesk.com/docs/en/client/linux/#login-screen')); + } + if (LinuxCards.isNotEmpty) { + return Column( + children: LinuxCards, + ); + } + } + if (bind.isIncomingOnly()) { + return Align( + alignment: Alignment.centerRight, + child: OutlinedButton( + onPressed: () { + SystemNavigator.pop(); // Close the application + // https://github.com/flutter/flutter/issues/66631 + if (isWindows) { + exit(0); + } + }, + child: Text(translate('Quit')), + ), + ).marginAll(14); + } + return Container(); + } + + Widget buildInstallCard(String title, String content, String btnText, + GestureTapCallback onPressed, + {double marginTop = 20.0, + String? help, + String? link, + bool? closeButton, + String? closeOption}) { + if (bind.mainGetBuildinOption(key: kOptionHideHelpCards) == 'Y' && + content != 'install_daemon_tip') { + return const SizedBox(); + } + void closeCard() async { + if (closeOption != null) { + await bind.mainSetLocalOption(key: closeOption, value: 'N'); + if (bind.mainGetLocalOption(key: closeOption) == 'N') { + setState(() { + isCardClosed = true; + }); + } + } else { + setState(() { + isCardClosed = true; + }); + } + } + + return Stack( + children: [ + Container( + margin: EdgeInsets.fromLTRB( + 0, marginTop, 0, bind.isIncomingOnly() ? marginTop : 0), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Color.fromARGB(255, 226, 66, 188), + Color.fromARGB(255, 244, 114, 124), + ], + )), + padding: EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: (title.isNotEmpty + ? [ + Center( + child: Text( + translate(title), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15), + ).marginOnly(bottom: 6)), + ] + : []) + + [ + if (content.isNotEmpty) + Text( + translate(content), + style: TextStyle( + height: 1.5, + color: Colors.white, + fontWeight: FontWeight.normal, + fontSize: 13), + ).marginOnly(bottom: 20) + ] + + (btnText.isNotEmpty + ? [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FixedWidthButton( + width: 150, + padding: 8, + isOutline: true, + text: translate(btnText), + textColor: Colors.white, + borderColor: Colors.white, + textSize: 20, + radius: 10, + onTap: onPressed, + ) + ]) + ] + : []) + + (help != null + ? [ + Center( + child: InkWell( + onTap: () async => + await launchUrl(Uri.parse(link!)), + child: Text( + translate(help), + style: TextStyle( + decoration: + TextDecoration.underline, + color: Colors.white, + fontSize: 12), + )).marginOnly(top: 6)), + ] + : []))), + ), + if (closeButton != null && closeButton == true) + Positioned( + top: 18, + right: 0, + child: IconButton( + icon: Icon( + Icons.close, + color: Colors.white, + size: 20, + ), + onPressed: closeCard, + ), + ), + ], + ); + } + + @override + void initState() { + super.initState(); + _updateTimer = periodic_immediate(const Duration(seconds: 1), () async { + await gFFI.serverModel.fetchID(); + final error = await bind.mainGetError(); + if (systemError != error) { + systemError = error; + setState(() {}); + } + final v = await mainGetBoolOption(kOptionStopService); + if (v != svcStopped.value) { + svcStopped.value = v; + setState(() {}); + } + if (watchIsCanScreenRecording) { + if (bind.mainIsCanScreenRecording(prompt: false)) { + watchIsCanScreenRecording = false; + setState(() {}); + } + } + if (watchIsProcessTrust) { + if (bind.mainIsProcessTrusted(prompt: false)) { + watchIsProcessTrust = false; + setState(() {}); + } + } + if (watchIsInputMonitoring) { + if (bind.mainIsCanInputMonitoring(prompt: false)) { + watchIsInputMonitoring = false; + // Do not notify for now. + // Monitoring may not take effect until the process is restarted. + // rustDeskWinManager.call( + // WindowType.RemoteDesktop, kWindowDisableGrabKeyboard, ''); + setState(() {}); + } + } + if (watchIsCanRecordAudio) { + if (isMacOS) { + Future.microtask(() async { + if ((await osxCanRecordAudio() == + PermissionAuthorizeType.authorized)) { + watchIsCanRecordAudio = false; + setState(() {}); + } + }); + } else { + watchIsCanRecordAudio = false; + setState(() {}); + } + } + }); + Get.put(svcStopped, tag: 'stop-service'); + rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); + + screenToMap(window_size.Screen screen) => { + 'frame': { + 'l': screen.frame.left, + 't': screen.frame.top, + 'r': screen.frame.right, + 'b': screen.frame.bottom, + }, + 'visibleFrame': { + 'l': screen.visibleFrame.left, + 't': screen.visibleFrame.top, + 'r': screen.visibleFrame.right, + 'b': screen.visibleFrame.bottom, + }, + 'scaleFactor': screen.scaleFactor, + }; + + bool isChattyMethod(String methodName) { + switch (methodName) { + case kWindowBumpMouse: return true; + } + + return false; + } + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + if (!isChattyMethod(call.method)) { + debugPrint( + "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + } + if (call.method == kWindowMainWindowOnTop) { + windowOnTop(null); + } else if (call.method == kWindowRefreshCurrentUser) { + gFFI.userModel.refreshCurrentUser(); + } else if (call.method == kWindowGetWindowInfo) { + final screen = (await window_size.getWindowInfo()).screen; + if (screen == null) { + return ''; + } else { + return jsonEncode(screenToMap(screen)); + } + } else if (call.method == kWindowGetScreenList) { + return jsonEncode( + (await window_size.getScreenList()).map(screenToMap).toList()); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } else if (call.method == kWindowEventShow) { + await rustDeskWinManager.registerActiveWindow(call.arguments["id"]); + } else if (call.method == kWindowEventHide) { + await rustDeskWinManager.unregisterActiveWindow(call.arguments['id']); + } else if (call.method == kWindowConnect) { + await connectMainDesktop( + call.arguments['id'], + isFileTransfer: call.arguments['isFileTransfer'], + isViewCamera: call.arguments['isViewCamera'], + isTerminal: call.arguments['isTerminal'], + isTcpTunneling: call.arguments['isTcpTunneling'], + isRDP: call.arguments['isRDP'], + password: call.arguments['password'], + forceRelay: call.arguments['forceRelay'], + connToken: call.arguments['connToken'], + ); + } else if (call.method == kWindowBumpMouse) { + return RdPlatformChannel.instance.bumpMouse( + dx: call.arguments['dx'], + dy: call.arguments['dy']); + } else if (call.method == kWindowEventMoveTabToNewWindow) { + final args = call.arguments.split(','); + int? windowId; + try { + windowId = int.parse(args[0]); + } catch (e) { + debugPrint("Failed to parse window id '${call.arguments}': $e"); + } + WindowType? windowType; + try { + windowType = WindowType.values.byName(args[3]); + } catch (e) { + debugPrint("Failed to parse window type '${call.arguments}': $e"); + } + if (windowId != null && windowType != null) { + await rustDeskWinManager.moveTabToNewWindow( + windowId, args[1], args[2], windowType); + } + } else if (call.method == kWindowEventOpenMonitorSession) { + final args = jsonDecode(call.arguments); + final windowId = args['window_id'] as int; + final peerId = args['peer_id'] as String; + final display = args['display'] as int; + final displayCount = args['display_count'] as int; + final windowType = args['window_type'] as int; + final screenRect = parseParamScreenRect(args); + await rustDeskWinManager.openMonitorSession( + windowId, peerId, display, displayCount, screenRect, windowType); + } else if (call.method == kWindowEventRemoteWindowCoords) { + final windowId = int.tryParse(call.arguments); + if (windowId != null) { + return jsonEncode( + await rustDeskWinManager.getOtherRemoteWindowCoords(windowId)); + } + } + }); + _uniLinksSubscription = listenUniLinks(); + + if (bind.isIncomingOnly()) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateWindowSize(); + }); + } + WidgetsBinding.instance.addObserver(this); + } + + _updateWindowSize() { + RenderObject? renderObject = _childKey.currentContext?.findRenderObject(); + if (renderObject == null) { + return; + } + if (renderObject is RenderBox) { + final size = renderObject.size; + if (size != imcomingOnlyHomeSize) { + imcomingOnlyHomeSize = size; + windowManager.setSize(getIncomingOnlyHomeSize()); + } + } + } + + @override + void dispose() { + _uniLinksSubscription?.cancel(); + Get.delete(tag: 'stop-service'); + _updateTimer?.cancel(); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + shouldBeBlocked(_block, canBeBlocked); + } + } + + Widget buildPluginEntry() { + final entries = PluginUiManager.instance.entries.entries; + return Offstage( + offstage: entries.isEmpty, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...entries.map((entry) { + return entry.value; + }) + ], + ), + ); + } +} + +void setPasswordDialog({VoidCallback? notEmptyCallback}) async { + final p0 = TextEditingController(text: ""); + final p1 = TextEditingController(text: ""); + var errMsg0 = ""; + var errMsg1 = ""; + final localPasswordSet = + (await bind.mainGetCommon(key: "local-permanent-password-set")) == "true"; + final permanentPasswordSet = + (await bind.mainGetCommon(key: "permanent-password-set")) == "true"; + final presetPassword = permanentPasswordSet && !localPasswordSet; + var canSubmit = false; + final RxString rxPass = "".obs; + final rules = [ + DigitValidationRule(), + UppercaseValidationRule(), + LowercaseValidationRule(), + // SpecialCharacterValidationRule(), + MinCharactersValidationRule(8), + ]; + final maxLength = bind.mainMaxEncryptLen(); + final statusTip = localPasswordSet + ? translate('password-hidden-tip') + : (presetPassword ? translate('preset-password-in-use-tip') : ''); + final showStatusTipOnMobile = + statusTip.isNotEmpty && !isDesktop && !isWebDesktop; + + gFFI.dialogManager.show((setState, close, context) { + updateCanSubmit() { + canSubmit = p0.text.trim().isNotEmpty || p1.text.trim().isNotEmpty; + } + + submit() async { + if (!canSubmit) { + return; + } + setState(() { + errMsg0 = ""; + errMsg1 = ""; + }); + final pass = p0.text.trim(); + if (pass.isNotEmpty) { + final Iterable violations = rules.where((r) => !r.validate(pass)); + if (violations.isNotEmpty) { + setState(() { + errMsg0 = + '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'; + }); + return; + } + } + if (p1.text.trim() != pass) { + setState(() { + errMsg1 = + '${translate('Prompt')}: ${translate("The confirmation is not identical.")}'; + }); + return; + } + final ok = await bind.mainSetPermanentPasswordWithResult(password: pass); + if (!ok) { + setState(() { + errMsg0 = '${translate('Prompt')}: ${translate("Failed")}'; + }); + return; + } + if (pass.isNotEmpty) { + notEmptyCallback?.call(); + } + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.key, color: MyTheme.accent), + Text(translate("Set Password")).paddingOnly(left: 10), + ], + ), + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: showStatusTipOnMobile ? 0.0 : 6.0, + ), + Row( + children: [ + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + labelText: translate('Password'), + errorText: errMsg0.isNotEmpty ? errMsg0 : null), + controller: p0, + autofocus: true, + onChanged: (value) { + rxPass.value = value.trim(); + setState(() { + errMsg0 = ''; + updateCanSubmit(); + }); + }, + maxLength: maxLength, + ).workaroundFreezeLinuxMint(), + ), + ], + ), + Row( + children: [ + Expanded(child: PasswordStrengthIndicator(password: rxPass)), + ], + ).marginOnly(top: 2, bottom: showStatusTipOnMobile ? 2 : 8), + SizedBox( + height: showStatusTipOnMobile ? 0.0 : 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + labelText: translate('Confirmation'), + errorText: errMsg1.isNotEmpty ? errMsg1 : null), + controller: p1, + onChanged: (value) { + setState(() { + errMsg1 = ''; + updateCanSubmit(); + }); + }, + maxLength: maxLength, + ).workaroundFreezeLinuxMint(), + ), + ], + ), + if (statusTip.isNotEmpty) + Row( + children: [ + Icon(Icons.info, color: Colors.amber, size: 18) + .marginOnly(right: 6), + Expanded( + child: Text( + statusTip, + style: const TextStyle(fontSize: 13, height: 1.1), + )) + ], + ).marginOnly(top: 6, bottom: 2), + SizedBox( + height: showStatusTipOnMobile ? 0.0 : 8.0, + ), + Obx(() => Wrap( + runSpacing: showStatusTipOnMobile ? 2.0 : 8.0, + spacing: 4, + children: rules.map((e) { + var checked = e.validate(rxPass.value.trim()); + return Chip( + label: Text( + e.name, + style: TextStyle( + color: checked + ? const Color(0xFF0A9471) + : Color.fromARGB(255, 198, 86, 157)), + ), + backgroundColor: checked + ? const Color(0xFFD0F7ED) + : Color.fromARGB(255, 247, 205, 232)); + }).toList(), + )) + ], + ), + ), + actions: (() { + final cancelButton = dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ); + final removeButton = dialogButton( + "Remove", + icon: Icon(Icons.delete_outline_rounded), + onPressed: () async { + setState(() { + errMsg0 = ""; + errMsg1 = ""; + }); + final ok = + await bind.mainSetPermanentPasswordWithResult(password: ""); + if (!ok) { + setState(() { + errMsg0 = '${translate('Prompt')}: ${translate("Failed")}'; + }); + return; + } + close(); + }, + buttonStyle: ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.red)), + ); + final okButton = dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: canSubmit ? submit : null, + ); + if (!isDesktop && !isWebDesktop && localPasswordSet) { + return [ + Align( + alignment: Alignment.centerRight, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + cancelButton, + const SizedBox(width: 4), + removeButton, + const SizedBox(width: 4), + okButton, + ], + ), + ), + ), + ]; + } + return [ + cancelButton, + if (localPasswordSet) removeButton, + okButton, + ]; + })(), + onSubmit: canSubmit ? submit : null, + onCancel: close, + ); + }); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_setting_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_setting_page.dart new file mode 100644 index 0000000..d118b67 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -0,0 +1,3124 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/audio_input.dart'; +import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; +import 'package:flutter_hbb/mobile/widgets/dialog.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/printer_model.dart'; +import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/plugin/manager.dart'; +import 'package:flutter_hbb/plugin/widgets/desktop_settings.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/widgets/dialog.dart'; +import '../../common/widgets/login.dart'; + +const double _kTabWidth = 200; +const double _kTabHeight = 42; +const double _kCardFixedWidth = 540; +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; +const String _kSettingPageControllerTag = 'settingPageController'; +const String _kSettingPageTabKeyTag = 'settingPageTabKey'; + +class _TabInfo { + late final SettingsTabKey key; + late final String label; + late final IconData unselected; + late final IconData selected; + _TabInfo(this.key, this.label, this.unselected, this.selected); +} + +enum SettingsTabKey { + general, + safety, + network, + display, + plugin, + account, + printer, + about, +} + +class DesktopSettingPage extends StatefulWidget { + final SettingsTabKey initialTabkey; + static final List tabKeys = [ + SettingsTabKey.general, + if (!isWeb && + !bind.isOutgoingOnly() && + !bind.isDisableSettings() && + bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y') + SettingsTabKey.safety, + if (!bind.isDisableSettings() && + bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y') + SettingsTabKey.network, + if (!bind.isIncomingOnly()) SettingsTabKey.display, + if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) + SettingsTabKey.plugin, + if (!bind.isDisableAccount()) SettingsTabKey.account, + if (isWindows && + bind.mainGetBuildinOption(key: kOptionHideRemotePrinterSetting) != 'Y') + SettingsTabKey.printer, + SettingsTabKey.about, + ]; + + DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key); + + @override + State createState() => + _DesktopSettingPageState(initialTabkey); + + static void switch2page(SettingsTabKey page) { + try { + int index = tabKeys.indexOf(page); + if (index == -1) { + return; + } + if (Get.isRegistered(tag: _kSettingPageControllerTag)) { + DesktopTabPage.onAddSetting(initialPage: page); + PageController controller = + Get.find(tag: _kSettingPageControllerTag); + Rx selected = + Get.find>(tag: _kSettingPageTabKeyTag); + selected.value = page; + controller.jumpToPage(index); + } else { + DesktopTabPage.onAddSetting(initialPage: page); + } + } catch (e) { + debugPrintStack(label: '$e'); + } + } +} + +class _DesktopSettingPageState extends State + with + TickerProviderStateMixin, + AutomaticKeepAliveClientMixin, + WidgetsBindingObserver { + late PageController controller; + late Rx selectedTab; + + @override + bool get wantKeepAlive => true; + + final RxBool _block = false.obs; + final RxBool _canBeBlocked = false.obs; + Timer? _videoConnTimer; + + _DesktopSettingPageState(SettingsTabKey initialTabkey) { + var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey); + if (initialIndex == -1) { + initialIndex = 0; + } + selectedTab = DesktopSettingPage.tabKeys[initialIndex].obs; + Get.put>(selectedTab, tag: _kSettingPageTabKeyTag); + controller = PageController(initialPage: initialIndex); + Get.put(controller, tag: _kSettingPageControllerTag); + controller.addListener(() { + if (controller.page != null) { + int page = controller.page!.toInt(); + if (page < DesktopSettingPage.tabKeys.length) { + selectedTab.value = DesktopSettingPage.tabKeys[page]; + } + } + }); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + shouldBeBlocked(_block, canBeBlocked); + } + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _videoConnTimer = + periodic_immediate(Duration(milliseconds: 1000), () async { + if (!mounted) { + return; + } + _canBeBlocked.value = await canBeBlocked(); + }); + } + + @override + void dispose() { + super.dispose(); + Get.delete(tag: _kSettingPageControllerTag); + Get.delete(tag: _kSettingPageTabKeyTag); + WidgetsBinding.instance.removeObserver(this); + _videoConnTimer?.cancel(); + } + + List<_TabInfo> _settingTabs() { + final List<_TabInfo> settingTabs = <_TabInfo>[]; + for (final tab in DesktopSettingPage.tabKeys) { + switch (tab) { + case SettingsTabKey.general: + settingTabs.add(_TabInfo( + tab, 'General', Icons.settings_outlined, Icons.settings)); + break; + case SettingsTabKey.safety: + settingTabs.add(_TabInfo(tab, 'Security', + Icons.enhanced_encryption_outlined, Icons.enhanced_encryption)); + break; + case SettingsTabKey.network: + settingTabs + .add(_TabInfo(tab, 'Network', Icons.link_outlined, Icons.link)); + break; + case SettingsTabKey.display: + settingTabs.add(_TabInfo(tab, 'Display', + Icons.desktop_windows_outlined, Icons.desktop_windows)); + break; + case SettingsTabKey.plugin: + settingTabs.add(_TabInfo( + tab, 'Plugin', Icons.extension_outlined, Icons.extension)); + break; + case SettingsTabKey.account: + settingTabs.add( + _TabInfo(tab, 'Account', Icons.person_outline, Icons.person)); + break; + case SettingsTabKey.printer: + settingTabs + .add(_TabInfo(tab, 'Printer', Icons.print_outlined, Icons.print)); + break; + case SettingsTabKey.about: + settingTabs + .add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info)); + break; + } + } + return settingTabs; + } + + List _children() { + final children = List.empty(growable: true); + for (final tab in DesktopSettingPage.tabKeys) { + switch (tab) { + case SettingsTabKey.general: + children.add(const _General()); + break; + case SettingsTabKey.safety: + children.add(const _Safety()); + break; + case SettingsTabKey.network: + children.add(const _Network()); + break; + case SettingsTabKey.display: + children.add(const _Display()); + break; + case SettingsTabKey.plugin: + children.add(const _Plugin()); + break; + case SettingsTabKey.account: + children.add(const _Account()); + break; + case SettingsTabKey.printer: + children.add(const _Printer()); + break; + case SettingsTabKey.about: + children.add(const _About()); + break; + } + } + return children; + } + + Widget _buildBlock({required List children}) { + // check both mouseMoveTime and videoConnCount + return Obx(() { + final videoConnBlock = + _canBeBlocked.value && stateGlobal.videoConnCount > 0; + return Stack(children: [ + buildRemoteBlock( + block: _block, + mask: false, + use: canBeBlocked, + child: preventMouseKeyBuilder( + child: Row(children: children), + block: videoConnBlock, + ), + ), + if (videoConnBlock) + Container( + color: Colors.black.withOpacity(0.5), + ) + ]); + }); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: _buildBlock( + children: [ + SizedBox( + width: _kTabWidth, + child: Column( + children: [ + _header(context), + Flexible(child: _listView(tabs: _settingTabs())), + ], + ), + ), + const VerticalDivider(width: 1), + Expanded( + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: PageView( + controller: controller, + physics: NeverScrollableScrollPhysics(), + children: _children(), + ), + ), + ) + ], + ), + ); + } + + Widget _header(BuildContext context) { + final settingsText = Text( + translate('Settings'), + textAlign: TextAlign.left, + style: const TextStyle( + color: _accentColor, + fontSize: _kTitleFontSize, + fontWeight: FontWeight.w400, + ), + ); + return Row( + children: [ + if (isWeb) + IconButton( + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }, + icon: Icon(Icons.arrow_back), + ).marginOnly(left: 5), + if (isWeb) + SizedBox( + height: 62, + child: Align( + alignment: Alignment.center, + child: settingsText, + ), + ).marginOnly(left: 20), + if (!isWeb) + SizedBox( + height: 62, + child: settingsText, + ).marginOnly(left: 20, top: 10), + const Spacer(), + ], + ); + } + + Widget _listView({required List<_TabInfo> tabs}) { + final scrollController = ScrollController(); + return ListView( + controller: scrollController, + children: tabs.map((tab) => _listItem(tab: tab)).toList(), + ); + } + + Widget _listItem({required _TabInfo tab}) { + return Obx(() { + bool selected = tab.key == selectedTab.value; + return SizedBox( + width: _kTabWidth, + height: _kTabHeight, + child: InkWell( + onTap: () { + if (selectedTab.value != tab.key) { + int index = DesktopSettingPage.tabKeys.indexOf(tab.key); + if (index == -1) { + return; + } + controller.jumpToPage(index); + } + selectedTab.value = tab.key; + }, + 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 _General extends StatefulWidget { + const _General({Key? key}) : super(key: key); + + @override + State<_General> createState() => _GeneralState(); +} + +class _GeneralState extends State<_General> { + final RxBool serviceStop = + isWeb ? RxBool(false) : Get.find(tag: 'stop-service'); + RxBool serviceBtnEnabled = true.obs; + + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return ListView( + controller: scrollController, + children: [ + if (!isWeb) service(), + theme(), + _Card(title: 'Language', children: [language()]), + if (!isWeb) hwcodec(), + if (!isWeb) audio(context), + if (!isWeb) record(context), + if (!isWeb) WaylandCard(), + other() + ], + ).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget theme() { + final current = MyTheme.getThemeModePreference().toShortString(); + onChanged(String value) async { + await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); + setState(() {}); + } + + final isOptFixed = isOptionFixed(kCommConfKeyTheme); + return _Card(title: 'Theme', children: [ + _Radio(context, + value: 'light', + groupValue: current, + label: 'Light', + onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: 'dark', + groupValue: current, + label: 'Dark', + onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: 'system', + groupValue: current, + label: 'Follow System', + onChanged: isOptFixed ? null : onChanged), + ]); + } + + Widget service() { + if (bind.isOutgoingOnly()) { + return const Offstage(); + } + + final hideStopService = + bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y'; + + return Obx(() { + if (hideStopService && !serviceStop.value) { + return const Offstage(); + } + + return _Card(title: 'Service', children: [ + _Button(serviceStop.value ? 'Start' : 'Stop', () { + () async { + serviceBtnEnabled.value = false; + await start_service(serviceStop.value); + // enable the button after 1 second + Future.delayed(const Duration(seconds: 1), () { + serviceBtnEnabled.value = true; + }); + }(); + }, enabled: serviceBtnEnabled.value) + ]); + }); + } + + Widget other() { + final showAutoUpdate = isWindows && bind.mainIsInstalled(); + final children = [ + if (!isWeb && !bind.isIncomingOnly()) + _OptionCheckBox(context, 'Confirm before closing multiple tabs', + kOptionEnableConfirmClosingTabs, + isServer: false), + _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), + if (!isWeb) wallpaper(), + if (!isWeb && !bind.isIncomingOnly()) ...[ + _OptionCheckBox( + context, + 'Open connection in new tab', + kOptionOpenNewConnInTabs, + isServer: false, + ), + // though this is related to GUI, but opengl problem affects all users, so put in config rather than local + if (isLinux) + Tooltip( + message: translate('software_render_tip'), + child: _OptionCheckBox( + context, + "Always use software rendering", + kOptionAllowAlwaysSoftwareRender, + ), + ), + if (!isWeb) + Tooltip( + message: translate('texture_render_tip'), + child: _OptionCheckBox( + context, + "Use texture rendering", + kOptionTextureRender, + optGetter: bind.mainGetUseTextureRender, + optSetter: (k, v) async => + await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), + ), + ), + if (isWindows) + Tooltip( + message: translate('d3d_render_tip'), + child: _OptionCheckBox( + context, + "Use D3D rendering", + kOptionD3DRender, + isServer: false, + ), + ), + if (!isWeb && !bind.isCustomClient()) + _OptionCheckBox( + context, + 'Check for software update on startup', + kOptionEnableCheckUpdate, + isServer: false, + ), + if (showAutoUpdate) + _OptionCheckBox( + context, + 'Auto update', + kOptionAllowAutoUpdate, + isServer: true, + ), + if (isWindows && !bind.isOutgoingOnly()) + _OptionCheckBox( + context, + 'Capture screen using DirectX', + kOptionDirectxCapture, + ), + if (!bind.isIncomingOnly()) ...[ + _OptionCheckBox( + context, + 'Enable UDP hole punching', + kOptionEnableUdpPunch, + isServer: false, + ), + _OptionCheckBox( + context, + 'Enable IPv6 P2P connection', + kOptionEnableIpv6Punch, + isServer: false, + ), + ], + ], + ]; + + // Add client-side wakelock option for desktop platforms + if (!bind.isIncomingOnly()) { + children.add(_OptionCheckBox( + context, + 'keep-awake-during-outgoing-sessions-label', + kOptionKeepAwakeDuringOutgoingSessions, + isServer: false, + )); + } + + if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { + children.add(_OptionCheckBox( + context, 'Allow linux headless', kOptionAllowLinuxHeadless)); + } + if (!bind.isDisableAccount()) { + children.add(_OptionCheckBox( + context, + 'note-at-conn-end-tip', + kOptionAllowAskForNoteAtEndOfConnection, + isServer: false, + optSetter: (key, value) async { + if (value && !gFFI.userModel.isLogin) { + final res = await loginDialog(); + if (res != true) return; + } + await mainSetLocalBoolOption(key, value); + }, + )); + } + return _Card(title: 'Other', children: children); + } + + Widget wallpaper() { + if (bind.isOutgoingOnly()) { + return const Offstage(); + } + + return futureBuilder(future: () async { + final support = await bind.mainSupportRemoveWallpaper(); + return support; + }(), hasData: (data) { + if (data is bool && data == true) { + bool value = mainGetBoolOptionSync(kOptionAllowRemoveWallpaper); + return Row( + children: [ + Flexible( + child: _OptionCheckBox( + context, + 'Remove wallpaper during incoming sessions', + kOptionAllowRemoveWallpaper, + update: (bool v) { + setState(() {}); + }, + ), + ), + if (value) + _CountDownButton( + text: 'Test', + second: 5, + onPressed: () { + bind.mainTestWallpaper(second: 5); + }, + ) + ], + ); + } + + return Offstage(); + }); + } + + Widget hwcodec() { + final hwcodec = bind.mainHasHwcodec(); + final vram = bind.mainHasVram(); + return Offstage( + offstage: !(hwcodec || vram), + child: _Card(title: 'Hardware Codec', children: [ + _OptionCheckBox( + context, + 'Enable hardware codec', + kOptionEnableHwcodec, + update: (bool v) { + if (v) { + bind.mainCheckHwcodec(); + } + }, + ) + ]), + ); + } + + Widget audio(BuildContext context) { + if (bind.isOutgoingOnly()) { + return const Offstage(); + } + + builder(devices, currentDevice, setDevice) { + final child = ComboBox( + keys: devices, + values: devices, + initialKey: currentDevice, + onChanged: (key) async { + setDevice(key); + setState(() {}); + }, + ).marginOnly(left: _kContentHMargin); + return _Card(title: 'Audio Input Device', children: [child]); + } + + return AudioInput(builder: builder, isCm: false, isVoiceCall: false); + } + + Widget record(BuildContext context) { + final showRootDir = isWindows && bind.mainIsInstalled(); + return futureBuilder(future: () async { + String user_dir = bind.mainVideoSaveDirectory(root: false); + String root_dir = + showRootDir ? bind.mainVideoSaveDirectory(root: true) : ''; + bool user_dir_exists = await Directory(user_dir).exists(); + bool root_dir_exists = + showRootDir ? await Directory(root_dir).exists() : false; + return { + 'user_dir': user_dir, + 'root_dir': root_dir, + 'user_dir_exists': user_dir_exists, + 'root_dir_exists': root_dir_exists, + }; + }(), hasData: (data) { + Map map = data as Map; + String user_dir = map['user_dir']!; + String root_dir = map['root_dir']!; + bool root_dir_exists = map['root_dir_exists']!; + bool user_dir_exists = map['user_dir_exists']!; + return _Card(title: 'Recording', children: [ + if (!bind.isOutgoingOnly()) + _OptionCheckBox(context, 'Automatically record incoming sessions', + kOptionAllowAutoRecordIncoming), + if (!bind.isIncomingOnly()) + _OptionCheckBox(context, 'Automatically record outgoing sessions', + kOptionAllowAutoRecordOutgoing, + isServer: false), + if (showRootDir && !bind.isOutgoingOnly()) + Row( + children: [ + Text( + '${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'), + Expanded( + child: GestureDetector( + onTap: root_dir_exists + ? () => launchUrl(Uri.file(root_dir)) + : null, + child: Text( + root_dir, + softWrap: true, + style: root_dir_exists + ? const TextStyle( + decoration: TextDecoration.underline) + : null, + )).marginOnly(left: 10), + ), + ], + ).marginOnly(left: _kContentHMargin), + if (!(showRootDir && bind.isIncomingOnly())) + Row( + children: [ + Text( + '${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'), + Expanded( + child: GestureDetector( + onTap: user_dir_exists + ? () => launchUrl(Uri.file(user_dir)) + : null, + child: Text( + user_dir, + softWrap: true, + style: user_dir_exists + ? const TextStyle( + decoration: TextDecoration.underline) + : null, + )).marginOnly(left: 10), + ), + ElevatedButton( + onPressed: isOptionFixed(kOptionVideoSaveDirectory) + ? null + : () async { + String? initialDirectory; + if (await Directory.fromUri( + Uri.directory(user_dir)) + .exists()) { + initialDirectory = user_dir; + } + String? selectedDirectory = + await FilePicker.platform.getDirectoryPath( + initialDirectory: initialDirectory); + if (selectedDirectory != null) { + await bind.mainSetLocalOption( + key: kOptionVideoSaveDirectory, + value: selectedDirectory); + setState(() {}); + } + }, + child: Text(translate('Change'))) + .marginOnly(left: 5), + ], + ).marginOnly(left: _kContentHMargin), + ]); + }); + } + + Widget language() { + return futureBuilder(future: () async { + String langs = await bind.mainGetLangs(); + return {'langs': langs}; + }(), 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, defaultOptionLang); + values.insert(0, translate('Default')); + String currentKey = bind.mainGetLocalOption(key: kCommConfKeyLang); + if (!keys.contains(currentKey)) { + currentKey = defaultOptionLang; + } + final isOptFixed = isOptionFixed(kCommConfKeyLang); + return ComboBox( + keys: keys, + values: values, + initialKey: currentKey, + onChanged: (key) async { + await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key); + if (isWeb) reloadCurrentWindow(); + if (!isWeb) reloadAllWindows(); + if (!isWeb) bind.mainChangeLanguage(lang: key); + }, + enabled: !isOptFixed, + ).marginOnly(left: _kContentHMargin); + }); + } +} + +enum _AccessMode { + custom, + full, + view, +} + +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; + bool locked = bind.mainIsInstalled(); + final scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + super.build(context); + return SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + _lock(locked, 'Unlock Security Settings', () { + locked = false; + setState(() => {}); + }), + preventMouseKeyBuilder( + block: locked, + child: Column(children: [ + permissions(context), + password(context), + _Card(title: '2FA', children: [tfa()]), + if (!isChangeIdDisabled()) + _Card(title: 'ID', children: [changeId()]), + more(context), + ]), + ), + ], + )).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget tfa() { + bool enabled = !locked; + // Simple temp wrapper for PR check + tmpWrapper() { + RxBool has2fa = bind.mainHasValid2FaSync().obs; + RxBool hasBot = bind.mainHasValidBotSync().obs; + update() async { + has2fa.value = bind.mainHasValid2FaSync(); + setState(() {}); + } + + onChanged(bool? checked) async { + if (checked == false) { + CommonConfirmDialog( + gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () { + change2fa(callback: update); + }); + } else { + change2fa(callback: update); + } + } + + final tfa = GestureDetector( + child: InkWell( + child: Obx(() => Row( + children: [ + Checkbox( + value: has2fa.value, + onChanged: enabled ? onChanged : null) + .marginOnly(right: 5), + Expanded( + child: Text( + translate('enable-2fa-title'), + style: + TextStyle(color: disabledTextColor(context, enabled)), + )) + ], + )), + ), + onTap: () { + onChanged(!has2fa.value); + }, + ).marginOnly(left: _kCheckBoxLeftMargin); + if (!has2fa.value) { + return tfa; + } + updateBot() async { + hasBot.value = bind.mainHasValidBotSync(); + setState(() {}); + } + + onChangedBot(bool? checked) async { + if (checked == false) { + CommonConfirmDialog( + gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () { + changeBot(callback: updateBot); + }); + } else { + changeBot(callback: updateBot); + } + } + + final bot = GestureDetector( + child: Tooltip( + waitDuration: Duration(milliseconds: 300), + message: translate("enable-bot-tip"), + child: InkWell( + child: Obx(() => Row( + children: [ + Checkbox( + value: hasBot.value, + onChanged: enabled ? onChangedBot : null) + .marginOnly(right: 5), + Expanded( + child: Text( + translate('Telegram bot'), + style: TextStyle( + color: disabledTextColor(context, enabled)), + )) + ], + ))), + ), + onTap: () { + onChangedBot(!hasBot.value); + }, + ).marginOnly(left: _kCheckBoxLeftMargin + 30); + + final trust = Row( + children: [ + Flexible( + child: Tooltip( + waitDuration: Duration(milliseconds: 300), + message: translate("enable-trusted-devices-tip"), + child: _OptionCheckBox(context, "Enable trusted devices", + kOptionEnableTrustedDevices, + enabled: !locked, update: (v) { + setState(() {}); + }), + ), + ), + if (mainGetBoolOptionSync(kOptionEnableTrustedDevices)) + ElevatedButton( + onPressed: locked + ? null + : () { + manageTrustedDeviceDialog(); + }, + child: Text(translate('Manage trusted devices'))) + ], + ).marginOnly(left: 30); + + return Column( + children: [tfa, bot, trust], + ); + } + + return tmpWrapper(); + } + + Widget changeId() { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer(builder: ((context, model, child) { + return _Button('Change ID', changeIdDialog, + enabled: !locked && model.connectStatus > 0); + }))); + } + + Widget permissions(context) { + bool enabled = !locked; + // Simple temp wrapper for PR check + tmpWrapper() { + String accessMode = bind.mainGetOptionSync(key: kOptionAccessMode); + _AccessMode mode; + if (accessMode == 'full') { + mode = _AccessMode.full; + } else if (accessMode == 'view') { + mode = _AccessMode.view; + } else { + mode = _AccessMode.custom; + } + String initialKey; + bool? fakeValue; + switch (mode) { + case _AccessMode.custom: + initialKey = ''; + fakeValue = null; + break; + case _AccessMode.full: + initialKey = 'full'; + fakeValue = true; + break; + case _AccessMode.view: + initialKey = 'view'; + fakeValue = false; + break; + } + + return _Card(title: 'Permissions', children: [ + ComboBox( + keys: [ + defaultOptionAccessMode, + 'full', + 'view', + ], + values: [ + translate('Custom'), + translate('Full Access'), + translate('Screen Share'), + ], + enabled: enabled && !isOptionFixed(kOptionAccessMode), + initialKey: initialKey, + onChanged: (mode) async { + await bind.mainSetOption(key: kOptionAccessMode, value: mode); + setState(() {}); + }).marginOnly(left: _kContentHMargin), + Column( + children: [ + _OptionCheckBox( + context, 'Enable keyboard/mouse', kOptionEnableKeyboard, + enabled: enabled, fakeValue: fakeValue), + if (isWindows) + _OptionCheckBox( + context, 'Enable remote printer', kOptionEnableRemotePrinter, + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard, + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, 'Enable file transfer', kOptionEnableFileTransfer, + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable audio', kOptionEnableAudio, + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable camera', kOptionEnableCamera, + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable terminal', kOptionEnableTerminal, + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, 'Enable TCP tunneling', kOptionEnableTunnel, + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, 'Enable remote restart', kOptionEnableRemoteRestart, + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, 'Enable recording session', kOptionEnableRecordSession, + enabled: enabled, fakeValue: fakeValue), + if (isWindows) + _OptionCheckBox(context, 'Enable blocking user input', + kOptionEnableBlockInput, + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable remote configuration modification', + kOptionAllowRemoteConfigModification, + enabled: enabled, fakeValue: fakeValue), + ], + ), + ]); + } + + return tmpWrapper(); + } + + Widget password(BuildContext context) { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer(builder: ((context, model, child) { + List passwordKeys = [ + kUseTemporaryPassword, + kUsePermanentPassword, + kUseBothPasswords, + ]; + List passwordValues = [ + translate('Use one-time password'), + translate('Use permanent password'), + translate('Use both passwords'), + ]; + bool tmpEnabled = model.verificationMethod != kUsePermanentPassword; + bool permEnabled = model.verificationMethod != kUseTemporaryPassword; + String currentValue = + passwordValues[passwordKeys.indexOf(model.verificationMethod)]; + List radios = passwordValues + .map((value) => _Radio( + context, + value: value, + groupValue: currentValue, + label: value, + onChanged: locked + ? null + : ((value) async { + callback() async { + await model.setVerificationMethod( + passwordKeys[passwordValues.indexOf(value)]); + await model.updatePasswordModel(); + } + + if (value == + passwordValues[passwordKeys + .indexOf(kUsePermanentPassword)] && + (await bind.mainGetCommon( + key: "permanent-password-set")) != + "true") { + if (isChangePermanentPasswordDisabled()) { + await callback(); + return; + } + setPasswordDialog(notEmptyCallback: callback); + } else { + await callback(); + } + }), + )) + .toList(); + + var onChanged = tmpEnabled && !locked + ? (value) { + if (value != null) { + () async { + await model.setTemporaryPasswordLength(value.toString()); + await model.updatePasswordModel(); + }(); + } + } + : null; + List lengthRadios = ['6', '8', '10'] + .map((value) => GestureDetector( + child: Row( + children: [ + Radio( + value: value, + groupValue: model.temporaryPasswordLength, + onChanged: onChanged), + Text( + value, + style: TextStyle( + color: disabledTextColor( + context, onChanged != null)), + ), + ], + ).paddingOnly(right: 10), + onTap: () => onChanged?.call(value), + )) + .toList(); + + final isOptFixedNumOTP = + isOptionFixed(kOptionAllowNumericOneTimePassword); + final isNumOPTChangable = !isOptFixedNumOTP && tmpEnabled && !locked; + final numericOneTimePassword = GestureDetector( + child: InkWell( + child: Row( + children: [ + Checkbox( + value: model.allowNumericOneTimePassword, + onChanged: isNumOPTChangable + ? (bool? v) { + model.switchAllowNumericOneTimePassword(); + } + : null) + .marginOnly(right: 5), + Expanded( + child: Text( + translate('Numeric one-time password'), + style: TextStyle( + color: disabledTextColor(context, isNumOPTChangable)), + )) + ], + )), + onTap: isNumOPTChangable + ? () => model.switchAllowNumericOneTimePassword() + : null, + ).marginOnly(left: _kContentHSubMargin - 5); + + final modeKeys = [ + 'password', + 'click', + defaultOptionApproveMode + ]; + final modeValues = [ + translate('Accept sessions via password'), + translate('Accept sessions via click'), + translate('Accept sessions via both'), + ]; + var modeInitialKey = model.approveMode; + if (!modeKeys.contains(modeInitialKey)) { + modeInitialKey = defaultOptionApproveMode; + } + final usePassword = model.approveMode != 'click'; + + final isApproveModeFixed = isOptionFixed(kOptionApproveMode); + return _Card(title: 'Password', children: [ + ComboBox( + enabled: !locked && !isApproveModeFixed, + keys: modeKeys, + values: modeValues, + initialKey: modeInitialKey, + onChanged: (key) => model.setApproveMode(key), + ).marginOnly(left: _kContentHMargin), + if (usePassword) radios[0], + if (usePassword) + _SubLabeledWidget( + context, + 'One-time password length', + Row( + children: [ + ...lengthRadios, + ], + ), + enabled: tmpEnabled && !locked), + if (usePassword) numericOneTimePassword, + if (usePassword) radios[1], + if (usePassword && !isChangePermanentPasswordDisabled()) + _SubButton('Set permanent password', setPasswordDialog, + permEnabled && !locked), + // if (usePassword) + // hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6), + if (usePassword) radios[2], + ]); + }))); + } + + Widget more(BuildContext context) { + bool enabled = !locked; + return _Card(title: 'Security', children: [ + shareRdp(context, enabled), + _OptionCheckBox(context, 'Deny LAN discovery', 'enable-lan-discovery', + reverse: true, enabled: enabled), + ...directIp(context), + whitelist(), + ...autoDisconnect(context), + _OptionCheckBox(context, 'keep-awake-during-incoming-sessions-label', + kOptionKeepAwakeDuringIncomingSessions, + reverse: false, enabled: enabled), + if (bind.mainIsInstalled()) + _OptionCheckBox(context, 'allow-only-conn-window-open-tip', + 'allow-only-conn-window-open', + reverse: false, enabled: enabled), + if (bind.mainIsInstalled() && !isUnlockPinDisabled()) unlockPin() + ]); + } + + shareRdp(BuildContext context, bool enabled) { + onChanged(bool b) async { + await bind.mainSetShareRdp(enable: b); + setState(() {}); + } + + bool value = bind.mainIsShareRdp(); + return Offstage( + offstage: !(isWindows && bind.mainIsInstalled()), + child: GestureDetector( + child: Row( + children: [ + Checkbox( + value: value, + onChanged: enabled ? (_) => onChanged(!value) : null) + .marginOnly(right: 5), + Expanded( + child: Text(translate('Enable RDP session sharing'), + style: + TextStyle(color: disabledTextColor(context, enabled))), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: enabled ? () => onChanged(!value) : null), + ); + } + + List directIp(BuildContext context) { + TextEditingController controller = TextEditingController(); + update(bool v) => setState(() {}); + RxBool applyEnabled = false.obs; + return [ + _OptionCheckBox(context, 'Enable direct IP access', kOptionDirectServer, + update: update, enabled: !locked), + () { + // Simple temp wrapper for PR check + tmpWrapper() { + bool enabled = option2bool(kOptionDirectServer, + bind.mainGetOptionSync(key: kOptionDirectServer)); + if (!enabled) applyEnabled.value = false; + controller.text = + bind.mainGetOptionSync(key: kOptionDirectAccessPort); + final isOptFixed = isOptionFixed(kOptionDirectAccessPort); + return Offstage( + offstage: !enabled, + child: _SubLabeledWidget( + context, + 'Port', + Row(children: [ + SizedBox( + width: 95, + child: TextField( + controller: controller, + enabled: enabled && !locked && !isOptFixed, + onChanged: (_) => applyEnabled.value = true, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), + ], + decoration: const InputDecoration( + hintText: '21118', + contentPadding: + EdgeInsets.symmetric(vertical: 12, horizontal: 12), + ), + ).workaroundFreezeLinuxMint().marginOnly(right: 15), + ), + Obx(() => ElevatedButton( + onPressed: applyEnabled.value && + enabled && + !locked && + !isOptFixed + ? () async { + applyEnabled.value = false; + await bind.mainSetOption( + key: kOptionDirectAccessPort, + value: controller.text); + } + : null, + child: Text( + translate('Apply'), + ), + )) + ]), + enabled: enabled && !locked && !isOptFixed, + ), + ); + } + + return tmpWrapper(); + }(), + ]; + } + + Widget whitelist() { + bool enabled = !locked; + // Simple temp wrapper for PR check + tmpWrapper() { + RxBool hasWhitelist = whitelistNotEmpty().obs; + update() async { + hasWhitelist.value = whitelistNotEmpty(); + } + + onChanged(bool? checked) async { + changeWhiteList(callback: update); + } + + final isOptFixed = isOptionFixed(kOptionWhitelist); + return GestureDetector( + child: Tooltip( + message: translate('whitelist_tip'), + child: Obx(() => Row( + children: [ + Checkbox( + value: hasWhitelist.value, + onChanged: enabled && !isOptFixed ? onChanged : null) + .marginOnly(right: 5), + Offstage( + offstage: !hasWhitelist.value, + child: MouseRegion( + child: const Icon(Icons.warning_amber_rounded, + color: Color.fromARGB(255, 255, 204, 0)) + .marginOnly(right: 5), + cursor: SystemMouseCursors.click, + ), + ), + Expanded( + child: Text( + translate('Use IP Whitelisting'), + style: + TextStyle(color: disabledTextColor(context, enabled)), + )) + ], + )), + ), + onTap: enabled + ? () { + onChanged(!hasWhitelist.value); + } + : null, + ).marginOnly(left: _kCheckBoxLeftMargin); + } + + return tmpWrapper(); + } + + Widget hide_cm(bool enabled) { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer(builder: (context, model, child) { + final enableHideCm = model.approveMode == 'password' && + model.verificationMethod == kUsePermanentPassword; + onHideCmChanged(bool? b) { + if (b != null) { + bind.mainSetOption( + key: 'allow-hide-cm', value: bool2option('allow-hide-cm', b)); + } + } + + return Tooltip( + message: enableHideCm ? "" : translate('hide_cm_tip'), + child: GestureDetector( + onTap: + enableHideCm ? () => onHideCmChanged(!model.hideCm) : null, + child: Row( + children: [ + Checkbox( + value: model.hideCm, + onChanged: enabled && enableHideCm + ? onHideCmChanged + : null) + .marginOnly(right: 5), + Expanded( + child: Text( + translate('Hide connection management window'), + style: TextStyle( + color: disabledTextColor( + context, enabled && enableHideCm)), + ), + ), + ], + ), + )); + })); + } + + List autoDisconnect(BuildContext context) { + TextEditingController controller = TextEditingController(); + update(bool v) => setState(() {}); + RxBool applyEnabled = false.obs; + return [ + _OptionCheckBox( + context, 'auto_disconnect_option_tip', kOptionAllowAutoDisconnect, + update: update, enabled: !locked), + () { + bool enabled = option2bool(kOptionAllowAutoDisconnect, + bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect)); + if (!enabled) applyEnabled.value = false; + controller.text = + bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout); + final isOptFixed = isOptionFixed(kOptionAutoDisconnectTimeout); + return Offstage( + offstage: !enabled, + child: _SubLabeledWidget( + context, + 'Timeout in minutes', + Row(children: [ + SizedBox( + width: 95, + child: TextField( + controller: controller, + enabled: enabled && !locked && !isOptFixed, + onChanged: (_) => applyEnabled.value = true, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), + ], + decoration: const InputDecoration( + hintText: '10', + contentPadding: + EdgeInsets.symmetric(vertical: 12, horizontal: 12), + ), + ).workaroundFreezeLinuxMint().marginOnly(right: 15), + ), + Obx(() => ElevatedButton( + onPressed: + applyEnabled.value && enabled && !locked && !isOptFixed + ? () async { + applyEnabled.value = false; + await bind.mainSetOption( + key: kOptionAutoDisconnectTimeout, + value: controller.text); + } + : null, + child: Text( + translate('Apply'), + ), + )) + ]), + enabled: enabled && !locked && !isOptFixed, + ), + ); + }(), + ]; + } + + Widget unlockPin() { + bool enabled = !locked; + RxString unlockPin = bind.mainGetUnlockPin().obs; + update() async { + unlockPin.value = bind.mainGetUnlockPin(); + } + + onChanged(bool? checked) async { + changeUnlockPinDialog(unlockPin.value, update); + } + + final isOptFixed = isOptionFixed(kOptionWhitelist); + return GestureDetector( + child: Obx(() => Row( + children: [ + Checkbox( + value: unlockPin.isNotEmpty, + onChanged: enabled && !isOptFixed ? onChanged : null) + .marginOnly(right: 5), + Expanded( + child: Text( + translate('Unlock with PIN'), + style: TextStyle(color: disabledTextColor(context, enabled)), + )) + ], + )), + onTap: enabled + ? () { + onChanged(!unlockPin.isNotEmpty); + } + : null, + ).marginOnly(left: _kCheckBoxLeftMargin); + } +} + +class _Network extends StatefulWidget { + const _Network({Key? key}) : super(key: key); + + @override + State<_Network> createState() => _NetworkState(); +} + +class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + bool locked = !isWeb && bind.mainIsInstalled(); + + final scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + super.build(context); + return ListView(controller: scrollController, children: [ + _lock(locked, 'Unlock Network Settings', () { + locked = false; + setState(() => {}); + }), + preventMouseKeyBuilder( + block: locked, + child: Column(children: [ + network(context), + ]), + ), + ]).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget network(BuildContext context) { + final hideServer = + bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; + final hideProxy = + isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; + final hideWebSocket = isWeb || + bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y'; + + if (hideServer && hideProxy && hideWebSocket) { + return Offstage(); + } + + // Helper function to create network setting ListTiles + Widget listTile({ + required IconData icon, + required String title, + VoidCallback? onTap, + Widget? trailing, + bool showTooltip = false, + String tooltipMessage = '', + }) { + final titleWidget = showTooltip + ? Row( + children: [ + Tooltip( + waitDuration: Duration(milliseconds: 1000), + message: translate(tooltipMessage), + child: Row( + children: [ + Text( + translate(title), + style: TextStyle(fontSize: _kContentFontSize), + ), + SizedBox(width: 5), + Icon( + Icons.help_outline, + size: 14, + color: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.7), + ), + ], + ), + ), + ], + ) + : Text( + translate(title), + style: TextStyle(fontSize: _kContentFontSize), + ); + + return ListTile( + leading: Icon(icon, color: _accentColor), + title: titleWidget, + enabled: !locked, + onTap: onTap, + trailing: trailing, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16), + minLeadingWidth: 0, + horizontalTitleGap: 10, + ); + } + + Widget switchWidget(IconData icon, String title, String tooltipMessage, + String optionKey) => + listTile( + icon: icon, + title: title, + showTooltip: true, + tooltipMessage: tooltipMessage, + trailing: Switch( + value: mainGetBoolOptionSync(optionKey), + onChanged: locked || isOptionFixed(optionKey) + ? null + : (value) { + mainSetBoolOption(optionKey, value); + setState(() {}); + }, + ), + ); + + final outgoingOnly = bind.isOutgoingOnly(); + + final divider = const Divider(height: 1, indent: 16, endIndent: 16); + return _Card( + title: 'Network', + children: [ + Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!hideServer) + listTile( + icon: Icons.dns_outlined, + title: 'ID/Relay Server', + onTap: () => showServerSettings(gFFI.dialogManager, setState), + ), + if (!hideProxy && !hideServer) divider, + if (!hideProxy) + listTile( + icon: Icons.network_ping_outlined, + title: 'Socks5/Http(s) Proxy', + onTap: changeSocks5Proxy, + ), + if (!hideWebSocket && (!hideServer || !hideProxy)) divider, + if (!hideWebSocket) + switchWidget( + Icons.web_asset_outlined, + 'Use WebSocket', + '${translate('websocket_tip')}\n\n${translate('server-oss-not-support-tip')}', + kOptionAllowWebSocket), + if (!isWeb) + futureBuilder( + future: bind.mainIsUsingPublicServer(), + hasData: (isUsingPublicServer) { + if (isUsingPublicServer) { + return Offstage(); + } else { + return Column( + children: [ + if (!hideServer || !hideProxy || !hideWebSocket) + divider, + switchWidget( + Icons.no_encryption_outlined, + 'Allow insecure TLS fallback', + 'allow-insecure-tls-fallback-tip', + kOptionAllowInsecureTLSFallback), + if (!outgoingOnly) divider, + if (!outgoingOnly) + listTile( + icon: Icons.lan_outlined, + title: 'Disable UDP', + showTooltip: true, + tooltipMessage: + '${translate('disable-udp-tip')}\n\n${translate('server-oss-not-support-tip')}', + trailing: Switch( + value: bind.mainGetOptionSync( + key: kOptionDisableUdp) == + 'Y', + onChanged: + locked || isOptionFixed(kOptionDisableUdp) + ? null + : (value) async { + await bind.mainSetOption( + key: kOptionDisableUdp, + value: value ? 'Y' : 'N'); + setState(() {}); + }, + ), + ), + ], + ); + } + }, + ), + ], + ), + ), + ], + ); + } +} + +class _Display extends StatefulWidget { + const _Display({Key? key}) : super(key: key); + + @override + State<_Display> createState() => _DisplayState(); +} + +class _DisplayState extends State<_Display> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return ListView(controller: scrollController, children: [ + viewStyle(context), + scrollStyle(context), + imageQuality(context), + codec(context), + if (isDesktop) trackpadSpeed(context), + if (!isWeb) privacyModeImpl(context), + other(context), + ]).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget viewStyle(BuildContext context) { + final isOptFixed = isOptionFixed(kOptionViewStyle); + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: kOptionViewStyle, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: kOptionViewStyle); + return _Card(title: 'Default View Style', children: [ + _Radio(context, + value: kRemoteViewStyleOriginal, + groupValue: groupValue, + label: 'Scale original', + onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: kRemoteViewStyleAdaptive, + groupValue: groupValue, + label: 'Scale adaptive', + onChanged: isOptFixed ? null : onChanged), + ]); + } + + Widget scrollStyle(BuildContext context) { + final isOptFixed = isOptionFixed(kOptionScrollStyle); + onChanged(String value) async { + await bind.mainSetUserDefaultOption( + key: kOptionScrollStyle, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: kOptionScrollStyle); + + onEdgeScrollEdgeThicknessChanged(double value) async { + await bind.mainSetUserDefaultOption( + key: kOptionEdgeScrollEdgeThickness, value: value.round().toString()); + setState(() {}); + } + + return _Card(title: 'Default Scroll Style', children: [ + _Radio(context, + value: kRemoteScrollStyleAuto, + groupValue: groupValue, + label: 'ScrollAuto', + onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: kRemoteScrollStyleBar, + groupValue: groupValue, + label: 'Scrollbar', + onChanged: isOptFixed ? null : onChanged), + if (!isWeb) ...[ + _Radio(context, + value: kRemoteScrollStyleEdge, + groupValue: groupValue, + label: 'ScrollEdge', + onChanged: isOptFixed ? null : onChanged), + Offstage( + offstage: groupValue != kRemoteScrollStyleEdge, + child: EdgeThicknessControl( + value: double.tryParse(bind.mainGetUserDefaultOption( + key: kOptionEdgeScrollEdgeThickness)) ?? + 100.0, + onChanged: isOptionFixed(kOptionEdgeScrollEdgeThickness) + ? null + : onEdgeScrollEdgeThicknessChanged, + )), + ], + ]); + } + + Widget imageQuality(BuildContext context) { + onChanged(String value) async { + await bind.mainSetUserDefaultOption( + key: kOptionImageQuality, value: value); + setState(() {}); + } + + final isOptFixed = isOptionFixed(kOptionImageQuality); + final groupValue = bind.mainGetUserDefaultOption(key: kOptionImageQuality); + return _Card(title: 'Default Image Quality', children: [ + _Radio(context, + value: kRemoteImageQualityBest, + groupValue: groupValue, + label: 'Good image quality', + onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: kRemoteImageQualityBalanced, + groupValue: groupValue, + label: 'Balanced', + onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: kRemoteImageQualityLow, + groupValue: groupValue, + label: 'Optimize reaction time', + onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: kRemoteImageQualityCustom, + groupValue: groupValue, + label: 'Custom', + onChanged: isOptFixed ? null : onChanged), + Offstage( + offstage: groupValue != kRemoteImageQualityCustom, + child: customImageQualitySetting(), + ) + ]); + } + + Widget trackpadSpeed(BuildContext context) { + final initSpeed = + (int.tryParse(bind.mainGetUserDefaultOption(key: kKeyTrackpadSpeed)) ?? + kDefaultTrackpadSpeed); + final curSpeed = SimpleWrapper(initSpeed); + void onDebouncer(int v) { + bind.mainSetUserDefaultOption( + key: kKeyTrackpadSpeed, value: v.toString()); + // It's better to notify all sessions that the default speed is changed. + // But it may also be ok to take effect in the next connection. + } + + return _Card(title: 'Default trackpad speed', children: [ + TrackpadSpeedWidget( + value: curSpeed, + onDebouncer: onDebouncer, + ), + ]); + } + + Widget codec(BuildContext context) { + onChanged(String value) async { + await bind.mainSetUserDefaultOption( + key: kOptionCodecPreference, value: value); + setState(() {}); + } + + final groupValue = + bind.mainGetUserDefaultOption(key: kOptionCodecPreference); + var hwRadios = []; + final isOptFixed = isOptionFixed(kOptionCodecPreference); + try { + final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings()); + final h264 = codecsJson['h264'] ?? false; + final h265 = codecsJson['h265'] ?? false; + if (h264) { + hwRadios.add(_Radio(context, + value: 'h264', + groupValue: groupValue, + label: 'H264', + onChanged: isOptFixed ? null : onChanged)); + } + if (h265) { + hwRadios.add(_Radio(context, + value: 'h265', + groupValue: groupValue, + label: 'H265', + onChanged: isOptFixed ? null : onChanged)); + } + } catch (e) { + debugPrint("failed to parse supported hwdecodings, err=$e"); + } + return _Card(title: 'Default Codec', children: [ + _Radio(context, + value: 'auto', + groupValue: groupValue, + label: 'Auto', + onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: 'vp8', + groupValue: groupValue, + label: 'VP8', + onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: 'vp9', + groupValue: groupValue, + label: 'VP9', + onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: 'av1', + groupValue: groupValue, + label: 'AV1', + onChanged: isOptFixed ? null : onChanged), + ...hwRadios, + ]); + } + + Widget privacyModeImpl(BuildContext context) { + final supportedPrivacyModeImpls = bind.mainSupportedPrivacyModeImpls(); + late final List privacyModeImpls; + try { + privacyModeImpls = jsonDecode(supportedPrivacyModeImpls); + } catch (e) { + debugPrint('failed to parse supported privacy mode impls, err=$e'); + return Offstage(); + } + if (privacyModeImpls.length < 2) { + return Offstage(); + } + + final key = 'privacy-mode-impl-key'; + onChanged(String value) async { + await bind.mainSetOption(key: key, value: value); + setState(() {}); + } + + String groupValue = bind.mainGetOptionSync(key: key); + if (groupValue.isEmpty) { + groupValue = bind.mainDefaultPrivacyModeImpl(); + } + return _Card( + title: 'Privacy mode', + children: privacyModeImpls.map((impl) { + final d = impl as List; + return _Radio(context, + value: d[0] as String, + groupValue: groupValue, + label: d[1] as String, + onChanged: onChanged); + }).toList(), + ); + } + + Widget otherRow(String label, String key) { + final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; + final isOptFixed = isOptionFixed(key); + onChanged(bool b) async { + await bind.mainSetUserDefaultOption( + key: key, + value: b + ? 'Y' + : (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo)); + setState(() {}); + } + + return GestureDetector( + child: Row( + children: [ + Checkbox( + value: value, + onChanged: isOptFixed ? null : (_) => onChanged(!value)) + .marginOnly(right: 5), + Expanded( + child: Text(translate(label)), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: isOptFixed ? null : () => onChanged(!value)); + } + + Widget other(BuildContext context) { + final children = + otherDefaultSettings().map((e) => otherRow(e.$1, e.$2)).toList(); + return _Card(title: 'Other Default Options', children: children); + } +} + +class _Account extends StatefulWidget { + const _Account({Key? key}) : super(key: key); + + @override + State<_Account> createState() => _AccountState(); +} + +class _AccountState extends State<_Account> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return ListView( + controller: scrollController, + children: [ + _Card(title: 'Account', children: [accountAction(), useInfo()]), + ], + ).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget accountAction() { + return Obx(() => _Button( + gFFI.userModel.userName.value.isEmpty + ? 'Login' + : '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})', + () => { + gFFI.userModel.userName.value.isEmpty + ? loginDialog() + : logOutConfirmDialog() + })); + } + + Widget useInfo() { + return Obx(() => Offstage( + offstage: gFFI.userModel.userName.value.isEmpty, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10), + ), + child: Builder(builder: (context) { + final avatarWidget = _buildUserAvatar(); + return Row( + children: [ + if (avatarWidget != null) avatarWidget, + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + gFFI.userModel.displayNameOrUserName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + SelectionArea( + child: Text( + '@${gFFI.userModel.userName.value}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + color: + Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ), + ], + ), + ), + ], + ); + }), + ), + )).marginOnly(left: 18, top: 16); + } + + Widget? _buildUserAvatar() { + // Resolve relative avatar path at display time + final avatar = + bind.mainResolveAvatarUrl(avatar: gFFI.userModel.avatar.value); + return buildAvatarWidget( + avatar: avatar, + size: 44, + ); + } +} + +class _Checkbox extends StatefulWidget { + final String label; + final bool Function() getValue; + final Future Function(bool) setValue; + + const _Checkbox( + {Key? key, + required this.label, + required this.getValue, + required this.setValue}) + : super(key: key); + + @override + State<_Checkbox> createState() => _CheckboxState(); +} + +class _CheckboxState extends State<_Checkbox> { + var value = false; + + @override + initState() { + super.initState(); + value = widget.getValue(); + } + + @override + Widget build(BuildContext context) { + onChanged(bool b) async { + await widget.setValue(b); + setState(() { + value = widget.getValue(); + }); + } + + return GestureDetector( + child: Row( + children: [ + Checkbox( + value: value, + onChanged: (_) => onChanged(!value), + ).marginOnly(right: 5), + Expanded( + child: Text(translate(widget.label)), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: () => onChanged(!value), + ); + } +} + +class _Plugin extends StatefulWidget { + const _Plugin({Key? key}) : super(key: key); + + @override + State<_Plugin> createState() => _PluginState(); +} + +class _PluginState extends State<_Plugin> { + @override + Widget build(BuildContext context) { + bind.pluginListReload(); + final scrollController = ScrollController(); + return ChangeNotifierProvider.value( + value: pluginManager, + child: Consumer(builder: (context, model, child) { + return ListView( + controller: scrollController, + children: model.plugins.map((entry) => pluginCard(entry)).toList(), + ).marginOnly(bottom: _kListViewBottomMargin); + }), + ); + } + + Widget pluginCard(PluginInfo plugin) { + return ChangeNotifierProvider.value( + value: plugin, + child: Consumer( + builder: (context, model, child) => DesktopSettingsCard(plugin: model), + ), + ); + } + + Widget accountAction() { + return Obx(() => _Button( + gFFI.userModel.userName.value.isEmpty + ? 'Login' + : '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})', + () => { + gFFI.userModel.userName.value.isEmpty + ? loginDialog() + : logOutConfirmDialog() + })); + } +} + +class _Printer extends StatefulWidget { + const _Printer({super.key}); + + @override + State<_Printer> createState() => __PrinterState(); +} + +class __PrinterState extends State<_Printer> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return ListView(controller: scrollController, children: [ + outgoing(context), + incoming(context), + ]).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget outgoing(BuildContext context) { + final isSupportPrinterDriver = + bind.mainGetCommonSync(key: 'is-support-printer-driver') == 'true'; + + Widget tipOsNotSupported() { + return Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-os-requirement-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + Widget tipClientNotInstalled() { + return Align( + alignment: Alignment.topLeft, + child: + Text(translate('printer-requires-installed-{$appName}-client-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + Widget tipPrinterNotInstalled() { + final failedMsg = ''.obs; + platformFFI.registerEventHandler( + 'install-printer-res', 'install-printer-res', (evt) async { + if (evt['success'] as bool) { + setState(() {}); + } else { + failedMsg.value = evt['msg'] as String; + } + }, replace: true); + return Column(children: [ + Obx( + () => failedMsg.value.isNotEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-{$appName}-not-installed-tip')) + .marginOnly(bottom: 10.0), + ), + ), + Obx( + () => failedMsg.value.isEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(failedMsg.value, + style: DefaultTextStyle.of(context) + .style + .copyWith(color: Colors.red)) + .marginOnly(bottom: 10.0)), + ), + _Button('Install {$appName} Printer', () { + failedMsg.value = ''; + bind.mainSetCommon(key: 'install-printer', value: ''); + }) + ]).marginOnly(left: _kCardLeftMargin, bottom: 2.0); + } + + Widget tipReady() { + return Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-{$appName}-ready-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + final installed = bind.mainIsInstalled(); + // `is-printer-installed` may fail, but it's rare case. + // Add additional error message here if it's really needed. + final isPrinterInstalled = + bind.mainGetCommonSync(key: 'is-printer-installed') == 'true'; + + final List children = []; + if (!isSupportPrinterDriver) { + children.add(tipOsNotSupported()); + } else { + children.addAll([ + if (!installed) tipClientNotInstalled(), + if (installed && !isPrinterInstalled) tipPrinterNotInstalled(), + if (installed && isPrinterInstalled) tipReady() + ]); + } + return _Card(title: 'Outgoing Print Jobs', children: children); + } + + Widget incoming(BuildContext context) { + onRadioChanged(String value) async { + await bind.mainSetLocalOption( + key: kKeyPrinterIncomingJobAction, value: value); + setState(() {}); + } + + PrinterOptions printerOptions = PrinterOptions.load(); + return _Card(title: 'Incoming Print Jobs', children: [ + _Radio(context, + value: kValuePrinterIncomingJobDismiss, + groupValue: printerOptions.action, + label: 'Dismiss', + onChanged: onRadioChanged), + _Radio(context, + value: kValuePrinterIncomingJobDefault, + groupValue: printerOptions.action, + label: 'use-the-default-printer-tip', + onChanged: onRadioChanged), + _Radio(context, + value: kValuePrinterIncomingJobSelected, + groupValue: printerOptions.action, + label: 'use-the-selected-printer-tip', + onChanged: onRadioChanged), + if (printerOptions.printerNames.isNotEmpty) + ComboBox( + initialKey: printerOptions.printerName, + keys: printerOptions.printerNames, + values: printerOptions.printerNames, + enabled: printerOptions.action == kValuePrinterIncomingJobSelected, + onChanged: (value) async { + await bind.mainSetLocalOption( + key: kKeyPrinterSelected, value: value); + setState(() {}); + }, + ).marginOnly(left: 10), + _OptionCheckBox( + context, + 'auto-print-tip', + kKeyPrinterAllowAutoPrint, + isServer: false, + enabled: printerOptions.action != kValuePrinterIncomingJobDismiss, + ) + ]); + } +} + +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(); + final buildDate = await bind.mainGetBuildDate(); + final fingerprint = await bind.mainGetFingerprint(); + return { + 'license': license, + 'version': version, + 'buildDate': buildDate, + 'fingerprint': fingerprint + }; + }(), hasData: (data) { + final license = data['license'].toString(); + final version = data['version'].toString(); + final buildDate = data['buildDate'].toString(); + final fingerprint = data['fingerprint'].toString(); + const linkStyle = TextStyle(decoration: TextDecoration.underline); + final scrollController = ScrollController(); + return SingleChildScrollView( + controller: scrollController, + child: _Card(title: translate('About RustDesk'), children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + SelectionArea( + child: Text('${translate('Version')}: $version') + .marginSymmetric(vertical: 4.0)), + SelectionArea( + child: Text('${translate('Build Date')}: $buildDate') + .marginSymmetric(vertical: 4.0)), + if (!isWeb) + SelectionArea( + child: Text('${translate('Fingerprint')}: $fingerprint') + .marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString('https://rustdesk.com/privacy.html'); + }, + child: Text( + translate('Privacy Statement'), + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString('https://rustdesk.com'); + }, + child: Text( + translate('Website'), + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + Container( + decoration: const BoxDecoration(color: Color(0xFF2c8cff)), + padding: + const EdgeInsets.symmetric(vertical: 24, horizontal: 8), + child: SelectionArea( + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license', + style: const TextStyle(color: Colors.white), + ), + Text( + translate('Slogan_tip'), + style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white), + ) + ], + ), + ), + ], + )), + ).marginSymmetric(vertical: 4.0) + ], + ).marginOnly(left: _kContentHMargin) + ]), + ); + }); + } +} + +//#endregion + +//#region components + +// ignore: non_constant_identifier_names +Widget _Card( + {required String title, + required List children, + List? title_suffix}) { + return Row( + children: [ + Flexible( + child: SizedBox( + width: _kCardFixedWidth, + child: Card( + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + translate(title), + textAlign: TextAlign.start, + style: const TextStyle( + fontSize: _kTitleFontSize, + ), + )), + ...?title_suffix + ], + ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10), + ...children + .map((e) => e.marginOnly(top: 4, right: _kContentHMargin)), + ], + ).marginOnly(bottom: 10), + ).marginOnly(left: _kCardLeftMargin, top: 15), + ), + ), + ], + ); +} + +// ignore: non_constant_identifier_names +Widget _OptionCheckBox( + BuildContext context, + String label, + String key, { + Function(bool)? update, + bool reverse = false, + bool enabled = true, + Icon? checkedIcon, + bool? fakeValue, + bool isServer = true, + bool Function()? optGetter, + Future Function(String, bool)? optSetter, +}) { + getOpt() => optGetter != null + ? optGetter() + : (isServer + ? mainGetBoolOptionSync(key) + : mainGetLocalBoolOptionSync(key)); + bool value = getOpt(); + final isOptFixed = isOptionFixed(key); + if (reverse) value = !value; + var ref = value.obs; + onChanged(option) async { + if (option != null) { + if (reverse) option = !option; + final setter = + optSetter ?? (isServer ? mainSetBoolOption : mainSetLocalBoolOption); + await setter(key, option); + final readOption = getOpt(); + if (reverse) { + ref.value = !readOption; + } else { + ref.value = readOption; + } + update?.call(readOption); + } + } + + if (fakeValue != null) { + ref.value = fakeValue; + enabled = false; + } + + return GestureDetector( + child: Obx( + () => Row( + children: [ + Checkbox( + value: ref.value, + onChanged: enabled && !isOptFixed ? onChanged : null) + .marginOnly(right: 5), + Offstage( + offstage: !ref.value || checkedIcon == null, + child: checkedIcon?.marginOnly(right: 5), + ), + Expanded( + child: Text( + translate(label), + style: TextStyle(color: disabledTextColor(context, enabled)), + )) + ], + ), + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: enabled && !isOptFixed + ? () { + onChanged(!ref.value); + } + : null, + ); +} + +// ignore: non_constant_identifier_names +Widget _Radio(BuildContext context, + {required T value, + required T groupValue, + required String label, + required Function(T value)? onChanged, + bool autoNewLine = true}) { + final onChange2 = onChanged != null + ? (T? value) { + if (value != null) { + onChanged(value); + } + } + : null; + return GestureDetector( + child: Row( + children: [ + Radio(value: value, groupValue: groupValue, onChanged: onChange2), + Expanded( + child: Text(translate(label), + overflow: autoNewLine ? null : TextOverflow.ellipsis, + style: TextStyle( + fontSize: _kContentFontSize, + color: disabledTextColor(context, onChange2 != null))) + .marginOnly(left: 5), + ), + ], + ).marginOnly(left: _kRadioLeftMargin), + onTap: () => onChange2?.call(value), + ); +} + +class WaylandCard extends StatefulWidget { + const WaylandCard({Key? key}) : super(key: key); + + @override + State createState() => _WaylandCardState(); +} + +class _WaylandCardState extends State { + final restoreTokenKey = 'wayland-restore-token'; + static const _kClearShortcutsInhibitorEventKey = + 'clear-gnome-shortcuts-inhibitor-permission-res'; + final _clearShortcutsInhibitorFailedMsg = ''.obs; + // Don't show the shortcuts permission reset button for now. + // Users can change it manually: + // "Settings" -> "Apps" -> "RustDesk" -> "Permissions" -> "Inhibit Shortcuts". + // For resetting(clearing) the permission from the portal permission store, you can + // use (replace with the RustDesk desktop file ID): + // busctl --user call org.freedesktop.impl.portal.PermissionStore \ + // /org/freedesktop/impl/portal/PermissionStore org.freedesktop.impl.portal.PermissionStore \ + // DeletePermission sss "gnome" "shortcuts-inhibitor" "" + // On a native install this is typically "rustdesk.desktop"; on Flatpak it is usually + // the exported desktop ID derived from the Flatpak app-id (e.g. "com.rustdesk.RustDesk.desktop"). + // + // We may add it back in the future if needed. + final showResetInhibitorPermission = false; + + @override + void initState() { + super.initState(); + if (showResetInhibitorPermission) { + platformFFI.registerEventHandler( + _kClearShortcutsInhibitorEventKey, _kClearShortcutsInhibitorEventKey, + (evt) async { + if (!mounted) return; + if (evt['success'] == true) { + setState(() {}); + } else { + _clearShortcutsInhibitorFailedMsg.value = + evt['msg'] as String? ?? 'Unknown error'; + } + }); + } + } + + @override + void dispose() { + if (showResetInhibitorPermission) { + platformFFI.unregisterEventHandler( + _kClearShortcutsInhibitorEventKey, _kClearShortcutsInhibitorEventKey); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return futureBuilder( + future: bind.mainHandleWaylandScreencastRestoreToken( + key: restoreTokenKey, value: "get"), + hasData: (restoreToken) { + final hasShortcutsPermission = showResetInhibitorPermission && + bind.mainGetCommonSync( + key: "has-gnome-shortcuts-inhibitor-permission") == + "true"; + + final children = [ + if (restoreToken.isNotEmpty) + _buildClearScreenSelection(context, restoreToken), + if (hasShortcutsPermission) + _buildClearShortcutsInhibitorPermission(context), + ]; + return Offstage( + offstage: children.isEmpty, + child: _Card(title: 'Wayland', children: children), + ); + }, + ); + } + + Widget _buildClearScreenSelection(BuildContext context, String restoreToken) { + onConfirm() async { + final msg = await bind.mainHandleWaylandScreencastRestoreToken( + key: restoreTokenKey, value: "clear"); + gFFI.dialogManager.dismissAll(); + if (msg.isNotEmpty) { + msgBox(gFFI.sessionId, 'custom-nocancel', 'Error', msg, '', + gFFI.dialogManager); + } else { + setState(() {}); + } + } + + showConfirmMsgBox() => msgBoxCommon( + gFFI.dialogManager, + 'Confirmation', + Text( + translate('confirm_clear_Wayland_screen_selection_tip'), + ), + [ + dialogButton('OK', onPressed: onConfirm), + dialogButton('Cancel', + onPressed: () => gFFI.dialogManager.dismissAll()) + ]); + + return _Button( + 'Clear Wayland screen selection', + showConfirmMsgBox, + tip: 'clear_Wayland_screen_selection_tip', + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.error.withOpacity(0.75)), + ), + ); + } + + Widget _buildClearShortcutsInhibitorPermission(BuildContext context) { + onConfirm() { + _clearShortcutsInhibitorFailedMsg.value = ''; + bind.mainSetCommon( + key: "clear-gnome-shortcuts-inhibitor-permission", value: ""); + gFFI.dialogManager.dismissAll(); + } + + showConfirmMsgBox() => msgBoxCommon( + gFFI.dialogManager, + 'Confirmation', + Text( + translate('confirm-clear-shortcuts-inhibitor-permission-tip'), + ), + [ + dialogButton('OK', onPressed: onConfirm), + dialogButton('Cancel', + onPressed: () => gFFI.dialogManager.dismissAll()) + ]); + + return Column(children: [ + Obx( + () => _clearShortcutsInhibitorFailedMsg.value.isEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(_clearShortcutsInhibitorFailedMsg.value, + style: DefaultTextStyle.of(context) + .style + .copyWith(color: Colors.red)) + .marginOnly(bottom: 10.0)), + ), + _Button( + 'Reset keyboard shortcuts permission', + showConfirmMsgBox, + tip: 'clear-shortcuts-inhibitor-permission-tip', + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.error.withOpacity(0.75)), + ), + ), + ]); + } +} + +// ignore: non_constant_identifier_names +Widget _Button(String label, Function() onPressed, + {bool enabled = true, String? tip, ButtonStyle? style}) { + var button = ElevatedButton( + onPressed: enabled ? onPressed : null, + child: Text( + translate(label), + ).marginSymmetric(horizontal: 15), + style: style, + ); + StatefulWidget child; + if (tip == null) { + child = button; + } else { + child = Tooltip(message: translate(tip), child: button); + } + return Row(children: [ + child, + ]).marginOnly(left: _kContentHMargin); +} + +// ignore: non_constant_identifier_names +Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { + return Row( + children: [ + ElevatedButton( + onPressed: enabled ? onPressed : null, + child: Text( + translate(label), + ).marginSymmetric(horizontal: 15), + ), + ], + ).marginOnly(left: _kContentHSubMargin); +} + +// ignore: non_constant_identifier_names +Widget _SubLabeledWidget(BuildContext context, String label, Widget child, + {bool enabled = true}) { + return Row( + children: [ + Text( + '${translate(label)}: ', + style: TextStyle(color: disabledTextColor(context, enabled)), + ), + SizedBox( + width: 10, + ), + child, + ], + ).marginOnly(left: _kContentHSubMargin); +} + +Widget _lock( + bool locked, + String label, + Function() onUnlock, +) { + return Offstage( + offstage: !locked, + child: Row( + children: [ + Flexible( + child: SizedBox( + width: _kCardFixedWidth, + child: Card( + child: ElevatedButton( + child: SizedBox( + height: 25, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.security_sharp, + size: 20, + ), + Text(translate(label)).marginOnly(left: 5), + ]).marginSymmetric(vertical: 2)), + onPressed: () async { + final unlockPin = bind.mainGetUnlockPin(); + if (unlockPin.isEmpty || isUnlockPinDisabled()) { + bool checked = await callMainCheckSuperUserPermission(); + if (checked) { + onUnlock(); + } + } else { + checkUnlockPinDialog(unlockPin, onUnlock); + } + }, + ).marginSymmetric(horizontal: 2, vertical: 4), + ).marginOnly(left: _kCardLeftMargin), + ).marginOnly(top: 10), + ), + ], + )); +} + +_LabeledTextField( + BuildContext context, + String label, + TextEditingController controller, + String errorText, + bool enabled, + bool secure) { + return Table( + columnWidths: const { + 0: FixedColumnWidth(150), + 1: FlexColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + Padding( + padding: const EdgeInsets.only(right: 10), + child: Text( + '${translate(label)}:', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 16, + color: disabledTextColor(context, enabled), + ), + ), + ), + TextField( + controller: controller, + enabled: enabled, + obscureText: secure, + autocorrect: false, + decoration: InputDecoration( + errorText: errorText.isNotEmpty ? errorText : null, + ), + style: TextStyle( + color: disabledTextColor(context, enabled), + ), + ).workaroundFreezeLinuxMint(), + ], + ), + ], + ).marginOnly(bottom: 8); +} + +class _CountDownButton extends StatefulWidget { + _CountDownButton({ + Key? key, + required this.text, + required this.second, + required this.onPressed, + }) : super(key: key); + final String text; + final VoidCallback? onPressed; + final int second; + + @override + State<_CountDownButton> createState() => _CountDownButtonState(); +} + +class _CountDownButtonState extends State<_CountDownButton> { + bool _isButtonDisabled = false; + + late int _countdownSeconds = widget.second; + + Timer? _timer; + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + void _startCountdownTimer() { + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + if (_countdownSeconds <= 0) { + setState(() { + _isButtonDisabled = false; + }); + timer.cancel(); + } else { + setState(() { + _countdownSeconds--; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: _isButtonDisabled + ? null + : () { + widget.onPressed?.call(); + setState(() { + _isButtonDisabled = true; + _countdownSeconds = widget.second; + }); + _startCountdownTimer(); + }, + child: Text( + _isButtonDisabled ? '$_countdownSeconds s' : translate(widget.text), + ), + ); + } +} + +//#endregion + +//#region dialogs + +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 proxyController = TextEditingController(text: proxy); + var userController = TextEditingController(text: username); + var pwdController = TextEditingController(text: password); + RxBool obscure = true.obs; + + // proxy settings + // The following option is a not real key, it is just used for custom client advanced settings. + const String optionProxyUrl = "proxy-url"; + final isOptFixed = isOptionFixed(optionProxyUrl); + + var isInProgress = false; + gFFI.dialogManager.show((setState, close, context) { + submit() async { + setState(() { + proxyMsg = ''; + isInProgress = true; + }); + cancel() { + setState(() { + isInProgress = false; + }); + } + + proxy = proxyController.text.trim(); + username = userController.text.trim(); + password = pwdController.text.trim(); + + if (proxy.isNotEmpty) { + String domainPort = proxy; + if (domainPort.contains('://')) { + domainPort = domainPort.split('://')[1]; + } + proxyMsg = translate(await bind.mainTestIfValidServer( + server: domainPort, testWithProxy: false)); + if (proxyMsg.isEmpty) { + // ignore + } else { + cancel(); + return; + } + } + await bind.mainSetSocks( + proxy: proxy, username: username, password: password); + close(); + } + + return CustomAlertDialog( + title: Text(translate('Socks5/Http(s) Proxy')), + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (!isMobile) + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Align( + alignment: Alignment.centerRight, + child: Row( + children: [ + Text( + translate('Server'), + ).marginOnly(right: 4), + Tooltip( + waitDuration: Duration(milliseconds: 0), + message: translate("default_proxy_tip"), + child: Icon( + Icons.help_outline_outlined, + size: 16, + color: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5), + ), + ), + ], + )).marginOnly(right: 10), + ), + Expanded( + child: TextField( + decoration: InputDecoration( + errorText: proxyMsg.isNotEmpty ? proxyMsg : null, + labelText: isMobile ? translate('Server') : null, + helperText: + isMobile ? translate("default_proxy_tip") : null, + helperMaxLines: isMobile ? 3 : null, + ), + controller: proxyController, + autofocus: true, + enabled: !isOptFixed, + ).workaroundFreezeLinuxMint(), + ), + ], + ).marginOnly(bottom: 8), + Row( + children: [ + if (!isMobile) + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), + Expanded( + child: TextField( + controller: userController, + decoration: InputDecoration( + labelText: isMobile ? translate('Username') : null, + ), + enabled: !isOptFixed, + ).workaroundFreezeLinuxMint(), + ), + ], + ).marginOnly(bottom: 8), + Row( + children: [ + if (!isMobile) + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Password")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), + Expanded( + child: Obx(() => TextField( + obscureText: obscure.value, + decoration: InputDecoration( + labelText: isMobile ? translate('Password') : null, + suffixIcon: IconButton( + onPressed: () => obscure.value = !obscure.value, + icon: Icon(obscure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: pwdController, + enabled: !isOptFixed, + maxLength: bind.mainMaxEncryptLen(), + ).workaroundFreezeLinuxMint()), + ), + ], + ), + // NOT use Offstage to wrap LinearProgressIndicator + if (isInProgress) + const LinearProgressIndicator().marginOnly(top: 8), + ], + ), + ), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + if (!isOptFixed) dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +//#endregion diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_tab_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_tab_page.dart new file mode 100644 index 0000000..6440e55 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -0,0 +1,119 @@ +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:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:get/get.dart'; +import 'package:window_manager/window_manager.dart'; +// import 'package:flutter/services.dart'; + +import '../../common/shared_state.dart'; + +class DesktopTabPage extends StatefulWidget { + const DesktopTabPage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopTabPageState(); + + static void onAddSetting( + {SettingsTabKey initialPage = SettingsTabKey.general}) { + try { + DesktopTabController tabController = Get.find(); + tabController.add(TabInfo( + key: kTabLabelSettingPage, + label: kTabLabelSettingPage, + selectedIcon: Icons.build_sharp, + unselectedIcon: Icons.build_outlined, + page: DesktopSettingPage( + key: const ValueKey(kTabLabelSettingPage), + initialTabkey: initialPage, + ))); + } catch (e) { + debugPrintStack(label: '$e'); + } + } +} + +class _DesktopTabPageState extends State { + final tabController = DesktopTabController(tabType: DesktopTabType.main); + + _DesktopTabPageState() { + RemoteCountState.init(); + Get.put(tabController); + tabController.add(TabInfo( + key: kTabLabelHomePage, + label: kTabLabelHomePage, + selectedIcon: Icons.home_sharp, + unselectedIcon: Icons.home_outlined, + closable: false, + page: DesktopHomePage( + key: const ValueKey(kTabLabelHomePage), + ))); + if (bind.isIncomingOnly()) { + tabController.onSelected = (key) { + if (key == kTabLabelHomePage) { + windowManager.setSize(getIncomingOnlyHomeSize()); + setResizable(false); + } else { + windowManager.setSize(getIncomingOnlySettingsSize()); + setResizable(true); + } + }; + } + } + + @override + void initState() { + super.initState(); + // HardwareKeyboard.instance.addHandler(_handleKeyEvent); + } + + /* + bool _handleKeyEvent(KeyEvent event) { + if (!mouseIn && event is KeyDownEvent) { + print('key down: ${event.logicalKey}'); + shouldBeBlocked(_block, canBeBlocked); + } + return false; // allow it to propagate + } + */ + + @override + void dispose() { + // HardwareKeyboard.instance.removeHandler(_handleKeyEvent); + Get.delete(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final tabWidget = Container( + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + tail: Offstage( + offstage: bind.isIncomingOnly() || bind.isDisableSettings(), + child: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + onTap: DesktopTabPage.onAddSetting, + isClose: false, + ), + ), + ))); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : Obx( + () => DragToResizeArea( + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: windowManagerEnableResizeEdges, + child: tabWidget, + ), + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/file_manager_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/file_manager_page.dart new file mode 100644 index 0000000..cf97351 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/file_manager_page.dart @@ -0,0 +1,1694 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:extended_text/extended_text.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; +import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; +import 'package:percent_indicator/percent_indicator.dart'; +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; +import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/file_model.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:flutter_hbb/web/dummy.dart' + if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart'; + +import '../../consts.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; +import '../../common.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../widgets/popup_menu.dart'; + +/// status of location bar +enum LocationStatus { + /// normal bread crumb bar + bread, + + /// show path text field + pathLocation, + + /// show file search bar text field + fileSearchBar +} + +/// The status of currently focused scope of the mouse +enum MouseFocusScope { + /// Mouse is in local field. + local, + + /// Mouse is in remote field. + remote, + + /// Mouse is not in local field, remote neither. + none +} + +class FileManagerPage extends StatefulWidget { + FileManagerPage( + {Key? key, + required this.id, + required this.password, + required this.isSharedPassword, + this.tabController, + this.connToken, + this.forceRelay}) + : super(key: key); + final String id; + final String? password; + final bool? isSharedPassword; + final bool? forceRelay; + final String? connToken; + final DesktopTabController? tabController; + final SimpleWrapper?> _lastState = SimpleWrapper(null); + + FFI get ffi => (_lastState.value! as _FileManagerPageState)._ffi; + + @override + State createState() { + final state = _FileManagerPageState(); + _lastState.value = state; + return state; + } +} + +class _FileManagerPageState extends State + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { + final _mouseFocusScope = Rx(MouseFocusScope.none); + + final _dropMaskVisible = false.obs; // TODO impl drop mask + final _overlayKeyState = OverlayKeyState(); + final _uniqueKey = UniqueKey(); + + late FFI _ffi; + + FileModel get model => _ffi.fileModel; + JobController get jobController => model.jobController; + + @override + void initState() { + super.initState(); + _ffi = FFI(null); + _ffi.start(widget.id, + isFileTransfer: true, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + connToken: widget.connToken, + forceRelay: widget.forceRelay); + WidgetsBinding.instance.addPostFrameCallback((_) { + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + Get.put(_ffi, tag: 'ft_${widget.id}'); + WakelockManager.enable(_uniqueKey); + if (isWeb) { + _ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id); + } + debugPrint("File manager page init success with id ${widget.id}"); + _ffi.dialogManager.setOverlayState(_overlayKeyState); + // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.tabController?.onSelected?.call(widget.id); + }); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + model.close().whenComplete(() { + _ffi.close(); + _ffi.dialogManager.dismissAll(); + WakelockManager.disable(_uniqueKey); + Get.delete(tag: 'ft_${widget.id}'); + }); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + bool get wantKeepAlive => true; + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + jobController.jobTable.refresh(); + } + } + + Widget willPopScope(Widget child) { + if (isWeb) { + return WillPopScope( + onWillPop: () async { + clientClose(_ffi.sessionId, _ffi); + return false; + }, + child: child, + ); + } else { + return child; + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Overlay(key: _overlayKeyState.key, initialEntries: [ + OverlayEntry(builder: (_) { + return willPopScope(Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: Row( + children: [ + if (!isWeb) + Flexible( + flex: 3, + child: dropArea(FileManagerView( + model.localController, _ffi, _mouseFocusScope))), + Flexible( + flex: 3, + child: dropArea(FileManagerView( + model.remoteController, _ffi, _mouseFocusScope))), + Flexible(flex: 2, child: statusList()) + ], + ), + )); + }) + ]); + } + + Widget dropArea(FileManagerView fileView) { + return DropTarget( + onDragDone: (detail) => + handleDragDone(detail, fileView.controller.isLocal), + onDragEntered: (enter) { + _dropMaskVisible.value = true; + }, + onDragExited: (exit) { + _dropMaskVisible.value = false; + }, + child: fileView); + } + + Widget generateCard(Widget child) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: child, + ); + } + + /// transfer status list + /// watch transfer status + Widget statusList() { + Widget getIcon(JobProgress job) { + final color = Theme.of(context).tabBarTheme.labelColor; + switch (job.type) { + case JobType.deleteDir: + case JobType.deleteFile: + return Icon(Icons.delete_outline, color: color); + default: + return Transform.rotate( + angle: isWeb + ? job.isRemoteToLocal + ? pi / 2 + : pi / 2 * 3 + : job.isRemoteToLocal + ? pi + : 0, + child: Icon(Icons.arrow_forward_ios, color: color), + ); + } + } + + statusListView(List jobs) => ListView.builder( + controller: ScrollController(), + itemBuilder: (BuildContext context, int index) { + final item = jobs[index]; + final status = item.getStatus(); + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: generateCard( + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + getIcon(item) + .marginSymmetric(horizontal: 10, vertical: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Tooltip( + waitDuration: Duration(milliseconds: 500), + message: item.jobName, + child: ExtendedText( + item.jobName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + overflowWidget: TextOverflowWidget( + child: Text("..."), + position: TextOverflowPosition.start), + ), + ), + Tooltip( + waitDuration: Duration(milliseconds: 500), + message: status, + child: Text(status, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + )).marginOnly(top: 6), + ), + Offstage( + offstage: item.type != JobType.transfer || + item.state != JobState.inProgress, + child: LinearPercentIndicator( + animateFromLastPercent: true, + center: Text(item.percentText), + barRadius: Radius.circular(15), + percent: item.percent, + progressColor: MyTheme.accent, + backgroundColor: Theme.of(context).hoverColor, + lineHeight: kDesktopFileTransferRowHeight, + ).paddingSymmetric(vertical: 8), + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Offstage( + offstage: item.state != JobState.paused, + child: MenuButton( + tooltip: translate("Resume"), + onPressed: () { + jobController.resumeJob(item.id); + }, + child: SvgPicture.asset( + "assets/refresh.svg", + colorFilter: svgColor(Colors.white), + ), + color: MyTheme.accent, + hoverColor: MyTheme.accent80, + ), + ), + MenuButton( + tooltip: translate("Delete"), + child: SvgPicture.asset( + "assets/close.svg", + colorFilter: svgColor(Colors.white), + ), + onPressed: () { + jobController.jobTable.removeAt(index); + jobController.cancelJob(item.id); + }, + color: MyTheme.accent, + hoverColor: MyTheme.accent80, + ), + ], + ).marginAll(12), + ], + ), + ], + ), + ), + ); + }, + itemCount: jobController.jobTable.length, + ); + + return PreferredSize( + preferredSize: const Size(200, double.infinity), + child: Container( + margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), + padding: const EdgeInsets.all(8.0), + child: Obx( + () => jobController.jobTable.isEmpty + ? generateCard( + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + "assets/transfer.svg", + colorFilter: svgColor( + Theme.of(context).tabBarTheme.labelColor), + height: 40, + ).paddingOnly(bottom: 10), + Text( + translate("No transfers in progress"), + textAlign: TextAlign.center, + textScaler: TextScaler.linear(1.20), + style: TextStyle( + color: + Theme.of(context).tabBarTheme.labelColor), + ), + ], + ), + ), + ) + : statusListView(jobController.jobTable), + )), + ); + } + + void handleDragDone(DropDoneDetails details, bool isLocal) { + if (isLocal) { + // ignore local + return; + } + final items = SelectedItems(isLocal: false); + for (var file in details.files) { + final f = File(file.path); + items.add(Entry() + ..path = file.path + ..name = file.name + ..size = FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync()); + } + final otherSideData = model.localController.directoryData(); + model.remoteController.sendFiles(items, otherSideData); + } +} + +class FileManagerView extends StatefulWidget { + final FileController controller; + final FFI _ffi; + final Rx _mouseFocusScope; + + FileManagerView(this.controller, this._ffi, this._mouseFocusScope); + + @override + State createState() => _FileManagerViewState(); +} + +class _FileManagerViewState extends State { + final _locationStatus = LocationStatus.bread.obs; + final _locationNode = FocusNode(); + final _locationBarKey = GlobalKey(); + final _searchText = "".obs; + final _breadCrumbScroller = ScrollController(); + final _keyboardNode = FocusNode(); + final _listSearchBuffer = TimeoutStringBuffer(); + final _nameColWidth = 0.0.obs; + final _modifiedColWidth = 0.0.obs; + final _sizeColWidth = 0.0.obs; + final _fileListScrollController = ScrollController(); + final _globalHeaderKey = GlobalKey(); + + /// [_lastClickTime], [_lastClickEntry] help to handle double click + var _lastClickTime = + DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000; + Entry? _lastClickEntry; + + double? _windowWidthPrev; + double _fileTransferMinimumWidth = 0.0; + + FileController get controller => widget.controller; + bool get isLocal => widget.controller.isLocal; + FFI get _ffi => widget._ffi; + SelectedItems get selectedItems => controller.selectedItems; + + @override + void initState() { + super.initState(); + // register location listener + _locationNode.addListener(onLocationFocusChanged); + controller.directory.listen((e) => breadCrumbScrollToEnd()); + } + + @override + void dispose() { + _locationNode.removeListener(onLocationFocusChanged); + _locationNode.dispose(); + _keyboardNode.dispose(); + _breadCrumbScroller.dispose(); + _fileListScrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + _handleColumnPorportions(); + return Container( + margin: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + headTools(), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: MouseRegion( + onEnter: (evt) { + widget._mouseFocusScope.value = isLocal + ? MouseFocusScope.local + : MouseFocusScope.remote; + _keyboardNode.requestFocus(); + }, + onExit: (evt) => + widget._mouseFocusScope.value = MouseFocusScope.none, + child: _buildFileList(context, _fileListScrollController), + )) + ], + ), + ), + ], + ), + ); + } + + void _handleColumnPorportions() { + final windowWidthNow = MediaQuery.of(context).size.width; + if (_windowWidthPrev == null) { + _windowWidthPrev = windowWidthNow; + final defaultColumnWidth = windowWidthNow * 0.115; + _fileTransferMinimumWidth = defaultColumnWidth / 3; + _nameColWidth.value = defaultColumnWidth; + _modifiedColWidth.value = defaultColumnWidth; + _sizeColWidth.value = defaultColumnWidth; + } + + if (_windowWidthPrev != windowWidthNow) { + final difference = windowWidthNow / _windowWidthPrev!; + _windowWidthPrev = windowWidthNow; + _fileTransferMinimumWidth *= difference; + _nameColWidth.value *= difference; + _modifiedColWidth.value *= difference; + _sizeColWidth.value *= difference; + } + } + + void onLocationFocusChanged() { + debugPrint("focus changed on local"); + if (_locationNode.hasFocus) { + // ignore + } else { + // lost focus, change to bread + if (_locationStatus.value != LocationStatus.fileSearchBar) { + _locationStatus.value = LocationStatus.bread; + } + } + } + + Widget headTools() { + var uploadButtonTapPosition = RelativeRect.fill; + RxBool isUploadFolder = + (bind.mainGetLocalOption(key: 'upload-folder-button') == 'Y').obs; + return Container( + child: Column( + children: [ + // symbols + PreferredSize( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + color: MyTheme.accent, + ), + padding: EdgeInsets.all(8.0), + child: FutureBuilder( + future: bind.sessionGetPlatform( + sessionId: _ffi.sessionId, + isRemote: !isLocal), + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.data!.isNotEmpty) { + return getPlatformImage('${snapshot.data}'); + } else { + return CircularProgressIndicator( + color: Theme.of(context) + .tabBarTheme + .labelColor, + ); + } + })), + Text(isLocal + ? translate("Local Computer") + : translate("Remote Computer")) + .marginOnly(left: 8.0) + ], + ), + preferredSize: Size(double.infinity, 70)) + .paddingOnly(bottom: 15), + // buttons + Row( + children: [ + Row( + children: [ + MenuButton( + tooltip: translate('Back'), + padding: EdgeInsets.only( + right: 3, + ), + child: RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + colorFilter: + svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + onPressed: () { + selectedItems.clear(); + controller.goBack(); + }, + ), + MenuButton( + tooltip: translate('Parent directory'), + child: RotatedBox( + quarterTurns: 3, + child: SvgPicture.asset( + "assets/arrow.svg", + colorFilter: + svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + onPressed: () { + selectedItems.clear(); + controller.goToParentDirectory(); + }, + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 3.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 2.5), + child: GestureDetector( + onTap: () { + _locationStatus.value = + _locationStatus.value == LocationStatus.bread + ? LocationStatus.pathLocation + : LocationStatus.bread; + Future.delayed(Duration.zero, () { + if (_locationStatus.value == + LocationStatus.pathLocation) { + _locationNode.requestFocus(); + } + }); + }, + child: Obx( + () => Container( + child: Row( + children: [ + Expanded( + child: _locationStatus.value == + LocationStatus.bread + ? buildBread() + : buildPathLocation()), + ], + ), + ), + ), + ), + ), + ), + ), + ), + Obx(() { + switch (_locationStatus.value) { + case LocationStatus.bread: + return MenuButton( + tooltip: translate('Search'), + onPressed: () { + _locationStatus.value = LocationStatus.fileSearchBar; + Future.delayed( + Duration.zero, () => _locationNode.requestFocus()); + }, + child: SvgPicture.asset( + "assets/search.svg", + colorFilter: + svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ); + case LocationStatus.pathLocation: + return MenuButton( + onPressed: null, + child: SvgPicture.asset( + "assets/close.svg", + colorFilter: + svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + color: Theme.of(context).disabledColor, + hoverColor: Theme.of(context).hoverColor, + ); + case LocationStatus.fileSearchBar: + return MenuButton( + tooltip: translate('Clear'), + onPressed: () { + onSearchText("", isLocal); + _locationStatus.value = LocationStatus.bread; + }, + child: SvgPicture.asset( + "assets/close.svg", + colorFilter: + svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ); + } + }), + MenuButton( + tooltip: translate('Refresh File'), + padding: EdgeInsets.only( + left: 3, + ), + onPressed: () { + controller.refresh(); + }, + child: SvgPicture.asset( + "assets/refresh.svg", + colorFilter: + svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + ], + ), + Row( + textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, + children: [ + Expanded( + child: Row( + mainAxisAlignment: + isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + MenuButton( + tooltip: translate('Home'), + padding: EdgeInsets.only( + right: 3, + ), + onPressed: () { + controller.goToHomeDirectory(); + }, + child: SvgPicture.asset( + "assets/home.svg", + colorFilter: + svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + MenuButton( + tooltip: translate('Create Folder'), + onPressed: () { + final name = TextEditingController(); + String? errorText; + _ffi.dialogManager.show((setState, close, context) { + name.addListener(() { + if (errorText != null) { + setState(() { + errorText = null; + }); + } + }); + submit() { + if (name.value.text.isNotEmpty) { + if (!PathUtil.validName(name.value.text, + controller.options.value.isWindows)) { + setState(() { + errorText = translate("Invalid folder name"); + }); + return; + } + controller.createDir(PathUtil.join( + controller.directory.value.path, + name.value.text, + controller.options.value.isWindows, + )); + close(); + } + } + + cancel() => close(false); + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset("assets/folder_new.svg", + colorFilter: svgColor(MyTheme.accent)), + Text( + translate("Create Folder"), + ).paddingOnly( + left: 10, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name", + ), + errorText: errorText, + ), + controller: name, + autofocus: true, + ).workaroundFreezeLinuxMint(), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "Ok", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }); + }, + child: SvgPicture.asset( + "assets/folder_new.svg", + colorFilter: + svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + Obx(() => MenuButton( + tooltip: translate('Delete'), + onPressed: SelectedItems.valid(selectedItems.items) + ? () async { + await (controller + .removeAction(selectedItems)); + selectedItems.clear(); + } + : null, + child: SvgPicture.asset( + "assets/trash.svg", + colorFilter: svgColor( + Theme.of(context).tabBarTheme.labelColor), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + )), + menu(isLocal: isLocal), + ], + ), + ), + if (isWeb) + Obx(() => ElevatedButton.icon( + style: ButtonStyle( + padding: MaterialStateProperty.all( + isLocal + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(right: 10)), + backgroundColor: MaterialStateProperty.all( + selectedItems.items.isEmpty + ? MyTheme.accent80 + : MyTheme.accent, + ), + ), + onPressed: () => + {webselectFiles(is_folder: isUploadFolder.value)}, + label: InkWell( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + focusColor: Colors.transparent, + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + uploadButtonTapPosition = + RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () async { + final value = await showMenu( + context: context, + position: uploadButtonTapPosition, + items: [ + PopupMenuItem( + value: false, + child: Text(translate('Upload files')), + ), + PopupMenuItem( + value: true, + child: Text(translate('Upload folder')), + ), + ]); + if (value != null) { + isUploadFolder.value = value; + bind.mainSetLocalOption( + key: 'upload-folder-button', + value: value ? 'Y' : ''); + webselectFiles(is_folder: value); + } + }, + child: Icon(Icons.arrow_drop_down), + ), + icon: Text( + translate(isUploadFolder.isTrue + ? 'Upload folder' + : 'Upload files'), + textAlign: TextAlign.right, + style: TextStyle( + color: Colors.white, + ), + ).marginOnly(left: 8), + )).marginOnly(left: 16), + Obx(() => ElevatedButton.icon( + style: ButtonStyle( + padding: MaterialStateProperty.all( + isLocal + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(right: 10)), + backgroundColor: MaterialStateProperty.all( + selectedItems.items.isEmpty + ? MyTheme.accent80 + : MyTheme.accent, + ), + ), + onPressed: SelectedItems.valid(selectedItems.items) + ? () { + final otherSideData = + controller.getOtherSideDirectoryData(); + controller.sendFiles(selectedItems, otherSideData); + selectedItems.clear(); + } + : null, + icon: isLocal + ? Text( + translate('Send'), + textAlign: TextAlign.right, + style: TextStyle( + color: selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white, + ), + ) + : isWeb + ? Offstage() + : RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + colorFilter: svgColor( + selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white), + alignment: Alignment.bottomRight, + ), + ), + label: isLocal + ? SvgPicture.asset( + "assets/arrow.svg", + colorFilter: svgColor(selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white), + ) + : Text( + translate(isWeb ? 'Download' : 'Receive'), + style: TextStyle( + color: selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white, + ), + ), + )), + ], + ).marginOnly(top: 8.0) + ], + ), + ); + } + + Widget menu({bool isLocal = false}) { + var menuPos = RelativeRect.fill; + + final List> items = [ + MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate("Show Hidden Files"), + getter: () async { + return controller.options.value.showHidden; + }, + setter: (bool v) async { + controller.toggleShowHidden(); + }, + padding: kDesktopMenuPadding, + dismissOnClicked: true, + ), + MenuEntryButton( + childBuilder: (style) => Text(translate("Select All"), style: style), + proc: () => setState(() => + selectedItems.selectAll(controller.directory.value.entries)), + padding: kDesktopMenuPadding, + dismissOnClicked: true), + MenuEntryButton( + childBuilder: (style) => + Text(translate("Unselect All"), style: style), + proc: () => selectedItems.clear(), + padding: kDesktopMenuPadding, + dismissOnClicked: true) + ]; + + return Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + child: MenuButton( + tooltip: translate('More'), + onPressed: () => mod_menu.showMenu( + context: context, + position: menuPos, + items: items + .map( + (e) => e.build( + context, + MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight), + ), + ) + .expand((i) => i) + .toList(), + elevation: 8, + ), + child: SvgPicture.asset( + "assets/dots.svg", + colorFilter: svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + ); + } + + Widget _buildFileList( + BuildContext context, ScrollController scrollController) { + final fd = controller.directory.value; + final entries = fd.entries; + Rx rightClickEntry = Rx(null); + + return ListSearchActionListener( + node: _keyboardNode, + buffer: _listSearchBuffer, + onNext: (buffer) { + debugPrint("searching next for $buffer"); + assert(buffer.length == 1); + assert(selectedItems.items.length <= 1); + var skipCount = 0; + if (selectedItems.items.isNotEmpty) { + final index = entries.indexOf(selectedItems.items.first); + if (index < 0) { + return; + } + skipCount = index + 1; + } + var searchResult = entries + .skip(skipCount) + .where((element) => element.name.toLowerCase().startsWith(buffer)); + if (searchResult.isEmpty) { + // cannot find next, lets restart search from head + debugPrint("restart search from head"); + searchResult = entries.where( + (element) => element.name.toLowerCase().startsWith(buffer)); + } + if (searchResult.isEmpty) { + selectedItems.clear(); + return; + } + _jumpToEntry(isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight); + }, + onSearch: (buffer) { + debugPrint("searching for $buffer"); + final selectedEntries = selectedItems; + final searchResult = entries + .where((element) => element.name.toLowerCase().startsWith(buffer)); + selectedEntries.clear(); + if (searchResult.isEmpty) { + selectedItems.clear(); + return; + } + _jumpToEntry(isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight); + }, + child: Obx(() { + final entries = controller.directory.value.entries; + final filteredEntries = _searchText.isNotEmpty + ? entries.where((element) { + return element.name.contains(_searchText.value); + }).toList(growable: false) + : entries; + final rows = filteredEntries.map((entry) { + final sizeStr = + entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; + final lastModifiedStr = entry.isDrive + ? " " + : "${entry.lastModified().toString().replaceAll(".000", "")} "; + var secondaryPosition = RelativeRect.fromLTRB(0, 0, 0, 0); + onTap() { + final items = selectedItems; + // handle double click + if (_checkDoubleClick(entry)) { + controller.openDirectory(entry.path); + items.clear(); + return; + } + _onSelectedChanged(items, filteredEntries, entry, isLocal); + } + + onSecondaryTap() { + final items = [ + if (!entry.isDrive && + versionCmp(_ffi.ffiModel.pi.version, "1.3.0") >= 0) + mod_menu.PopupMenuItem( + child: Text(translate("Rename")), + height: CustomPopupMenuTheme.height, + onTap: () { + controller.renameAction(entry, isLocal); + }, + ) + ]; + if (items.isNotEmpty) { + rightClickEntry.value = entry; + final future = mod_menu.showMenu( + context: context, + position: secondaryPosition, + items: items, + ); + future.then((value) { + rightClickEntry.value = null; + }); + future.onError((error, stackTrace) { + rightClickEntry.value = null; + }); + } + } + + onSecondaryTapDown(details) { + secondaryPosition = RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx, + details.globalPosition.dy); + } + + return Padding( + padding: EdgeInsets.symmetric(vertical: 1), + child: Obx(() => Container( + decoration: BoxDecoration( + color: selectedItems.items.contains(entry) + ? MyTheme.button + : Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(5.0), + ), + border: rightClickEntry.value == entry + ? Border.all( + color: MyTheme.button, + width: 1.0, + ) + : null, + ), + key: ValueKey(entry.name), + height: kDesktopFileTransferRowHeight, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: InkWell( + child: Row( + children: [ + GestureDetector( + child: Obx( + () => Container( + width: _nameColWidth.value, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: entry.name, + child: Row(children: [ + entry.isDrive + ? Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)) + .paddingAll(4) + : SvgPicture.asset( + entry.isFile + ? "assets/file.svg" + : "assets/folder.svg", + colorFilter: svgColor( + Theme.of(context) + .tabBarTheme + .labelColor), + ), + Expanded( + child: Text(entry.name.nonBreaking, + style: TextStyle( + color: selectedItems.items + .contains(entry) + ? Colors.white + : null), + overflow: + TextOverflow.ellipsis)) + ]), + )), + ), + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, + ), + SizedBox( + width: 2.0, + ), + GestureDetector( + child: Obx( + () => SizedBox( + width: _modifiedColWidth.value, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: selectedItems.items + .contains(entry) + ? Colors.white70 + : MyTheme.darkGray, + ), + )), + ), + ), + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, + ), + // Divider from header. + SizedBox( + width: 2.0, + ), + Expanded( + // width: 100, + child: GestureDetector( + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: sizeStr, + child: Text( + sizeStr, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10, + color: + selectedItems.items.contains(entry) + ? Colors.white70 + : MyTheme.darkGray), + ), + ), + onTap: onTap, + onSecondaryTap: onSecondaryTap, + onSecondaryTapDown: onSecondaryTapDown, + ), + ), + ], + ), + ), + ), + ], + ))), + ); + }).toList(growable: false); + + return Column( + children: [ + // Header + Row( + children: [ + Expanded(child: _buildFileBrowserHeader(context)), + ], + ), + // Body + Expanded( + child: ListView.builder( + controller: scrollController, + itemExtent: kDesktopFileTransferRowHeight, + itemBuilder: (context, index) { + return rows[index]; + }, + itemCount: rows.length, + ), + ), + ], + ); + }), + ); + } + + onSearchText(String searchText, bool isLocal) { + selectedItems.clear(); + _searchText.value = searchText; + } + + void _jumpToEntry(bool isLocal, Entry entry, + ScrollController scrollController, double rowHeight) { + final entries = controller.directory.value.entries; + final index = entries.indexOf(entry); + if (index == -1) { + debugPrint("entry is not valid: ${entry.path}"); + } + final selectedEntries = selectedItems; + final searchResult = entries.where((element) => element == entry); + selectedEntries.clear(); + if (searchResult.isEmpty) { + return; + } + final offset = min( + max(scrollController.position.minScrollExtent, + entries.indexOf(searchResult.first) * rowHeight), + scrollController.position.maxScrollExtent); + scrollController.jumpTo(offset); + selectedEntries.add(searchResult.first); + debugPrint("focused on ${searchResult.first.name}"); + } + + void _onSelectedChanged(SelectedItems selectedItems, List entries, + Entry entry, bool isLocal) { + final isCtrlDown = RawKeyboard.instance.keysPressed + .contains(LogicalKeyboardKey.controlLeft) || + RawKeyboard.instance.keysPressed + .contains(LogicalKeyboardKey.controlRight); + final isShiftDown = RawKeyboard.instance.keysPressed + .contains(LogicalKeyboardKey.shiftLeft) || + RawKeyboard.instance.keysPressed + .contains(LogicalKeyboardKey.shiftRight); + if (isCtrlDown) { + if (selectedItems.items.contains(entry)) { + selectedItems.remove(entry); + } else { + selectedItems.add(entry); + } + } else if (isShiftDown) { + final List indexGroup = []; + for (var selected in selectedItems.items) { + indexGroup.add(entries.indexOf(selected)); + } + indexGroup.add(entries.indexOf(entry)); + indexGroup.removeWhere((e) => e == -1); + final maxIndex = indexGroup.reduce(max); + final minIndex = indexGroup.reduce(min); + selectedItems.clear(); + entries + .getRange(minIndex, maxIndex + 1) + .forEach((e) => selectedItems.add(e)); + } else { + selectedItems.clear(); + selectedItems.add(entry); + } + setState(() {}); + } + + bool _checkDoubleClick(Entry entry) { + final current = DateTime.now().millisecondsSinceEpoch; + final elapsed = current - _lastClickTime; + _lastClickTime = current; + if (_lastClickEntry == entry) { + if (elapsed < bind.getDoubleClickTime()) { + return true; + } + } else { + _lastClickEntry = entry; + } + return false; + } + + void _onDrag(double dx, RxDouble column1, RxDouble column2) { + if (column1.value + dx <= _fileTransferMinimumWidth || + column2.value - dx <= _fileTransferMinimumWidth) { + return; + } + column1.value += dx; + column2.value -= dx; + column1.value = max(_fileTransferMinimumWidth, column1.value); + column2.value = max(_fileTransferMinimumWidth, column2.value); + } + + Widget _buildFileBrowserHeader(BuildContext context) { + final padding = EdgeInsets.all(1.0); + return SizedBox( + key: _globalHeaderKey, + height: kDesktopFileTransferHeaderHeight, + child: Row( + children: [ + Obx( + () => headerItemFunc( + _nameColWidth.value, SortBy.name, translate("Name")), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) => + _onDrag(dx, _nameColWidth, _modifiedColWidth), + padding: padding, + ), + Obx( + () => headerItemFunc(_modifiedColWidth.value, SortBy.modified, + translate("Modified")), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) => + _onDrag(dx, _modifiedColWidth, _sizeColWidth), + padding: padding), + Expanded( + child: headerItemFunc( + _sizeColWidth.value, SortBy.size, translate("Size"))) + ], + ), + ); + } + + Widget headerItemFunc(double? width, SortBy sortBy, String name) { + final headerTextStyle = + Theme.of(context).dataTableTheme.headingTextStyle ?? TextStyle(); + return ObxValue>( + (ascending) => InkWell( + onTap: () { + if (ascending.value == null) { + ascending.value = true; + } else { + ascending.value = !ascending.value!; + } + controller.changeSortStyle(sortBy, + isLocal: isLocal, ascending: ascending.value!); + }, + child: SizedBox( + width: width, + height: kDesktopFileTransferHeaderHeight, + child: Row( + children: [ + Expanded( + child: Text( + name, + style: headerTextStyle, + overflow: TextOverflow.ellipsis, + ).marginOnly(left: 4), + ), + ascending.value != null + ? Icon( + ascending.value! + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + ) + : SizedBox() + ], + ), + ), + ), () { + if (controller.sortBy.value == sortBy) { + return controller.sortAscending.obs; + } else { + return Rx(null); + } + }()); + } + + Widget buildBread() { + final items = getPathBreadCrumbItems(isLocal, (list) { + var path = ""; + for (var item in list) { + path = PathUtil.join(path, item, controller.options.value.isWindows); + } + controller.openDirectory(path); + }); + + return items.isEmpty + ? Offstage() + : Row( + key: _locationBarKey, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + final sc = _breadCrumbScroller; + final scale = isWindows ? 2 : 4; + sc.jumpTo(sc.offset + e.scrollDelta.dy / scale); + } + }, + child: BreadCrumb( + items: items, + divider: const Icon(Icons.keyboard_arrow_right_rounded), + overflow: ScrollableOverflow( + controller: _breadCrumbScroller, + ), + ), + ), + ), + ActionIcon( + message: "", + icon: Icons.keyboard_arrow_down_rounded, + onTap: () async { + final renderBox = _locationBarKey.currentContext + ?.findRenderObject() as RenderBox; + _locationBarKey.currentContext?.size; + + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + final x = offset.dx; + final y = offset.dy + size.height + 1; + + final isPeerWindows = controller.options.value.isWindows; + final List menuItems = [ + MenuEntryButton( + childBuilder: (TextStyle? style) => isPeerWindows + ? buildWindowsThisPC(context, style) + : Text( + '/', + style: style, + ), + proc: () { + controller.openDirectory('/'); + }, + dismissOnClicked: true), + MenuEntryDivider() + ]; + if (isPeerWindows) { + var loadingTag = ""; + if (!isLocal) { + loadingTag = _ffi.dialogManager.showLoading("Waiting"); + } + try { + final showHidden = controller.options.value.showHidden; + final fd = await controller.fileFetcher + .fetchDirectory("/", isLocal, showHidden); + for (var entry in fd.entries) { + menuItems.add(MenuEntryButton( + childBuilder: (TextStyle? style) => + Row(children: [ + Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)), + SizedBox(width: 10), + Text( + entry.name, + style: style, + ) + ]), + proc: () { + controller.openDirectory('${entry.name}\\'); + }, + dismissOnClicked: true)); + } + menuItems.add(MenuEntryDivider()); + } catch (e) { + debugPrint("buildBread fetchDirectory err=$e"); + } finally { + if (!isLocal) { + _ffi.dialogManager.dismissByTag(loadingTag); + } + } + } + mod_menu.showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + elevation: 4, + items: menuItems + .map((e) => e.build( + context, + MenuConfig( + commonColor: + CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: + CustomPopupMenuTheme.dividerHeight, + boxWidth: size.width))) + .expand((i) => i) + .toList()); + }, + iconSize: 20, + ) + ]); + } + + List getPathBreadCrumbItems( + bool isLocal, void Function(List) onPressed) { + final path = controller.directory.value.path; + final breadCrumbList = List.empty(growable: true); + final isWindows = controller.options.value.isWindows; + if (isWindows && path == '/') { + breadCrumbList.add(BreadCrumbItem( + content: TextButton( + child: buildWindowsThisPC(context), + style: ButtonStyle( + minimumSize: MaterialStateProperty.all(Size(0, 0))), + onPressed: () => onPressed(['/'])) + .marginSymmetric(horizontal: 4))); + } else { + final list = PathUtil.split(path, isWindows); + breadCrumbList.addAll( + list.asMap().entries.map( + (e) => BreadCrumbItem( + content: TextButton( + child: Text(e.value), + style: ButtonStyle( + minimumSize: MaterialStateProperty.all( + Size(0, 0), + ), + ), + onPressed: () => onPressed( + list.sublist(0, e.key + 1), + ), + ).marginSymmetric(horizontal: 4), + ), + ), + ); + } + return breadCrumbList; + } + + breadCrumbScrollToEnd() { + Future.delayed(Duration(milliseconds: 200), () { + if (_breadCrumbScroller.hasClients) { + _breadCrumbScroller.animateTo( + _breadCrumbScroller.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.fastLinearToSlowEaseIn); + } + }); + } + + Widget buildPathLocation() { + final text = _locationStatus.value == LocationStatus.pathLocation + ? controller.directory.value.path + : _searchText.value; + final textController = TextEditingController(text: text) + ..selection = TextSelection.collapsed(offset: text.length); + return Row( + children: [ + SvgPicture.asset( + _locationStatus.value == LocationStatus.pathLocation + ? "assets/folder.svg" + : "assets/search.svg", + colorFilter: svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + Expanded( + child: TextField( + focusNode: _locationNode, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + prefix: Padding( + padding: EdgeInsets.only(left: 4.0), + ), + ), + controller: textController, + onSubmitted: (path) { + controller.openDirectory(path); + }, + onChanged: _locationStatus.value == LocationStatus.fileSearchBar + ? (searchText) => onSearchText(searchText, isLocal) + : null, + ).workaroundFreezeLinuxMint(), + ) + ], + ); + } + + // openDirectory(String path, {bool isLocal = false}) { + // model.openDirectory(path, isLocal: isLocal); + // } +} + +Widget buildWindowsThisPC(BuildContext context, [TextStyle? textStyle]) { + final color = Theme.of(context).iconTheme.color?.withOpacity(0.7); + return Row(children: [ + Icon(Icons.computer, size: 20, color: color), + SizedBox(width: 10), + Text(translate('This PC'), style: textStyle) + ]); +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/file_manager_tab_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/file_manager_tab_page.dart new file mode 100644 index 0000000..ed3e968 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -0,0 +1,177 @@ +import 'dart:convert'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +import '../../models/platform_model.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 { + DesktopTabController get tabController => Get.find(); + + static const IconData selectedIcon = Icons.file_copy_sharp; + static const IconData unselectedIcon = Icons.file_copy_outlined; + + _FileManagerTabPageState(Map params) { + Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer)); + tabController.onSelected = (id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; + tabController.onRemoved = (_, id) => onRemoveId(id); + tabController.add(TabInfo( + key: params['id'], + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () async { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: params['id'], + tabController: tabController, + )) { + return; + } + tabController.closeBy(params['id']); + }, + page: FileManagerPage( + key: ValueKey(params['id']), + id: params['id'], + password: params['password'], + isSharedPassword: params['isSharedPassword'], + tabController: tabController, + forceRelay: params['forceRelay'], + connToken: params['connToken'], + ))); + } + + @override + void initState() { + super.initState(); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + debugPrint( + "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}"); + // for simplify, just replace connectionId + if (call.method == kWindowEventNewFileTransfer) { + final args = jsonDecode(call.arguments); + final id = args['id']; + windowOnTop(windowId()); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () async { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: id, + tabController: tabController, + )) { + return; + } + tabController.closeBy(id); + }, + page: FileManagerPage( + key: ValueKey(id), + id: id, + password: args['password'], + isSharedPassword: args['isSharedPassword'], + tabController: tabController, + forceRelay: args['forceRelay'], + connToken: args['connToken'], + ))); + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } + }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.FileTransfer, windowId: windowId()); + }); + } + + @override + Widget build(BuildContext context) { + final child = Scaffold( + backgroundColor: Theme.of(context).cardColor, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton(), + selectedBorderColor: MyTheme.accent, + labelGetter: DesktopTab.tablabelGetter, + )); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + )); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : SubWindowDragToResizeArea( + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, + windowId: stateGlobal.windowId, + ); + } + + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } + + Future handleWindowCloseButton() async { + final connLength = tabController.state.value.tabs.length; + if (connLength == 1) { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: tabController.state.value.tabs[0].key, + tabController: tabController, + )) { + return false; + } + } + if (connLength <= 1) { + tabController.clear(); + return true; + } else { + final bool res; + if (!option2bool(kOptionEnableConfirmClosingTabs, + bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + tabController.clear(); + } + return res; + } + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/install_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/install_page.dart new file mode 100644 index 0000000..5bf6baf --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/install_page.dart @@ -0,0 +1,274 @@ +import 'dart:convert'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:get/get.dart'; +import 'package:path/path.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:window_manager/window_manager.dart'; + +class InstallPage extends StatefulWidget { + const InstallPage({Key? key}) : super(key: key); + + @override + State createState() => _InstallPageState(); +} + +class _InstallPageState extends State { + final tabController = DesktopTabController(tabType: DesktopTabType.main); + + _InstallPageState() { + Get.put(tabController); + const label = "install"; + tabController.add(TabInfo( + key: label, + label: label, + closable: false, + page: _InstallPageBody( + key: const ValueKey(label), + ))); + } + + @override + void dispose() { + super.dispose(); + Get.delete(); + } + + @override + Widget build(BuildContext context) { + return DragToResizeArea( + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: windowManagerEnableResizeEdges, + child: Container( + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab(controller: tabController)), + ), + ); + } +} + +class _InstallPageBody extends StatefulWidget { + const _InstallPageBody({Key? key}) : super(key: key); + + @override + State<_InstallPageBody> createState() => _InstallPageBodyState(); +} + +class _InstallPageBodyState extends State<_InstallPageBody> + with WindowListener { + late final TextEditingController controller; + final RxBool startmenu = true.obs; + final RxBool desktopicon = true.obs; + final RxBool printer = true.obs; + final RxBool showProgress = false.obs; + final RxBool btnEnabled = true.obs; + + // todo move to theme. + final buttonStyle = OutlinedButton.styleFrom( + textStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.normal), + padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12), + ); + + _InstallPageBodyState() { + controller = TextEditingController(text: bind.installInstallPath()); + final installOptions = jsonDecode(bind.installInstallOptions()); + startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0'; + desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0'; + printer.value = installOptions['PRINTER'] != '0'; + } + + @override + void initState() { + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowClose() { + gFFI.close(); + super.onWindowClose(); + windowManager.setPreventClose(false); + windowManager.close(); + } + + InkWell Option(RxBool option, {String label = ''}) { + return InkWell( + // todo mouseCursor: "SystemMouseCursors.forbidden" or no cursor on btnEnabled == false + borderRadius: BorderRadius.circular(6), + onTap: () => btnEnabled.value ? option.value = !option.value : null, + child: Row( + children: [ + Obx( + () => Checkbox( + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + value: option.value, + onChanged: (v) => + btnEnabled.value ? option.value = !option.value : null, + ).marginOnly(right: 8), + ), + Expanded( + child: Text(translate(label)), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final double em = 13; + final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark; + return Scaffold( + backgroundColor: null, + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate('Installation'), + style: Theme.of(context).textTheme.headlineMedium), + Row( + children: [ + Text('${translate('Installation Path')}:') + .marginOnly(right: 10), + Expanded( + child: TextField( + controller: controller, + readOnly: true, + decoration: InputDecoration( + contentPadding: EdgeInsets.all(0.75 * em), + ), + ).workaroundFreezeLinuxMint().marginOnly(right: 10), + ), + Obx( + () => OutlinedButton.icon( + icon: Icon(Icons.folder_outlined, size: 16), + onPressed: btnEnabled.value ? selectInstallPath : null, + style: buttonStyle, + label: Text(translate('Change Path')), + ), + ) + ], + ).marginSymmetric(vertical: 2 * em), + Option(startmenu, label: 'Create start menu shortcuts') + .marginOnly(bottom: 7), + Option(desktopicon, label: 'Create desktop icon') + .marginOnly(bottom: 7), + Option(printer, label: 'Install {$appName} Printer'), + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: isDarkTheme + ? Color.fromARGB(135, 87, 87, 90) + : Colors.grey[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey), + ), + child: Row( + children: [ + Icon(Icons.info_outline_rounded, size: 32) + .marginOnly(right: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate('agreement_tip')) + .marginOnly(bottom: em), + InkWell( + hoverColor: Colors.transparent, + onTap: () => launchUrlString( + 'https://rustdesk.com/privacy.html'), + child: Tooltip( + message: 'https://rustdesk.com/privacy.html', + child: Row(children: [ + Icon(Icons.launch_outlined, size: 16) + .marginOnly(right: 5), + Text( + translate('End-user license agreement'), + style: const TextStyle( + decoration: TextDecoration.underline), + ) + ]), + ), + ), + ], + ) + ], + )).marginSymmetric(vertical: 2 * em), + Row( + children: [ + Expanded( + // NOT use Offstage to wrap LinearProgressIndicator + child: Obx(() => showProgress.value + ? LinearProgressIndicator().marginOnly(right: 10) + : Offstage()), + ), + Obx( + () => OutlinedButton.icon( + icon: Icon(Icons.close_rounded, size: 16), + label: Text(translate('Cancel')), + onPressed: + btnEnabled.value ? () => windowManager.close() : null, + style: buttonStyle, + ).marginOnly(right: 10), + ), + Obx( + () => ElevatedButton.icon( + icon: Icon(Icons.done_rounded, size: 16), + label: Text(translate('Accept and Install')), + onPressed: btnEnabled.value ? install : null, + style: buttonStyle, + ), + ), + Offstage( + offstage: bind.installShowRunWithoutInstall(), + child: Obx( + () => OutlinedButton.icon( + icon: Icon(Icons.screen_share_outlined, size: 16), + label: Text(translate('Run without install')), + onPressed: btnEnabled.value + ? () => bind.installRunWithoutInstall() + : null, + style: buttonStyle, + ).marginOnly(left: 10), + ), + ), + ], + ) + ], + ).paddingSymmetric(horizontal: 4 * em, vertical: 3 * em), + )); + } + + void install() { + do_install() { + btnEnabled.value = false; + showProgress.value = true; + String args = ''; + if (startmenu.value) args += ' startmenu'; + if (desktopicon.value) args += ' desktopicon'; + if (printer.value) args += ' printer'; + bind.installInstallMe(options: args, path: controller.text); + } + + do_install(); + } + + void selectInstallPath() async { + String? install_path = await FilePicker.platform + .getDirectoryPath(initialDirectory: controller.text); + if (install_path != null) { + controller.text = join(install_path, await bind.mainGetAppName()); + } + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/port_forward_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/port_forward_page.dart new file mode 100644 index 0000000..13dca0e --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/port_forward_page.dart @@ -0,0 +1,357 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; + +const double _kColumn1Width = 30; +const double _kColumn4Width = 100; +const double _kRowHeight = 60; +const double _kTextLeftMargin = 20; + +class _PortForward { + int localPort; + String remoteHost; + int remotePort; + + _PortForward.fromJson(List json) + : localPort = json[0] as int, + remoteHost = json[1] as String, + remotePort = json[2] as int; +} + +class PortForwardPage extends StatefulWidget { + PortForwardPage({ + Key? key, + required this.id, + required this.password, + required this.tabController, + required this.isRDP, + required this.isSharedPassword, + this.forceRelay, + this.connToken, + }) : super(key: key); + final String id; + final String? password; + final DesktopTabController tabController; + final bool isRDP; + final bool? forceRelay; + final bool? isSharedPassword; + final String? connToken; + final SimpleWrapper?> _lastState = SimpleWrapper(null); + + FFI get ffi => (_lastState.value! as _PortForwardPageState)._ffi; + + @override + State createState() { + final state = _PortForwardPageState(); + _lastState.value = state; + return state; + } +} + +class _PortForwardPageState extends State + with AutomaticKeepAliveClientMixin { + final TextEditingController localPortController = TextEditingController(); + final TextEditingController remoteHostController = TextEditingController(); + final TextEditingController remotePortController = TextEditingController(); + RxList<_PortForward> pfs = RxList.empty(growable: true); + late FFI _ffi; + + @override + void initState() { + super.initState(); + _ffi = FFI(null); + _ffi.start(widget.id, + isPortForward: true, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + connToken: widget.connToken, + isRdp: widget.isRDP); + Get.put(_ffi, tag: 'pf_${widget.id}'); + debugPrint("Port forward page init success with id ${widget.id}"); + // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.tabController.onSelected?.call(widget.id); + }); + } + + @override + void dispose() { + _ffi.close(); + _ffi.dialogManager.dismissAll(); + Get.delete(tag: 'pf_${widget.id}'); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: FutureBuilder(future: () async { + if (!widget.isRDP) { + refreshTunnelConfig(); + } + }(), builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Container( + decoration: BoxDecoration( + border: Border.all( + width: 20, + color: Theme.of(context).scaffoldBackgroundColor)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildPrompt(context), + Flexible( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + border: Border.all(width: 1, color: MyTheme.border)), + child: + widget.isRDP ? buildRdp(context) : buildTunnel(context), + ), + ), + ], + ), + ); + } + return const Offstage(); + }), + ); + } + + buildPrompt(BuildContext context) { + return Obx(() => Offstage( + offstage: pfs.isEmpty && !widget.isRDP, + child: Container( + height: 45, + color: const Color(0xFF007F00), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate('Listening ...'), + style: const TextStyle(fontSize: 16, color: Colors.white), + ), + Text( + translate('not_close_tcp_tip'), + style: const TextStyle( + fontSize: 10, color: Color(0xFFDDDDDD), height: 1.2), + ) + ])).marginOnly(bottom: 8), + )); + } + + buildTunnel(BuildContext context) { + text(String label) => Expanded( + child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); + + return Theme( + data: Theme.of(context).copyWith( + colorScheme: Theme.of(context).colorScheme, + ), + child: Obx(() => ListView.builder( + controller: ScrollController(), + itemCount: pfs.length + 2, + itemBuilder: ((context, index) { + if (index == 0) { + return Container( + height: 25, + color: Theme.of(context).scaffoldBackgroundColor, + child: Row(children: [ + text('Local Port'), + const SizedBox(width: _kColumn1Width), + text('Remote Host'), + text('Remote Port'), + SizedBox( + width: _kColumn4Width, child: Text(translate('Action'))) + ]), + ); + } else if (index == 1) { + return buildTunnelAddRow(context); + } else { + return buildTunnelDataRow(context, pfs[index - 2], index - 2); + } + }))), + ); + } + + buildTunnelAddRow(BuildContext context) { + var portInputFormatter = [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')) + ]; + + return Container( + height: _kRowHeight, + decoration: + BoxDecoration(color: Theme.of(context).colorScheme.background), + child: Row(children: [ + buildTunnelInputCell(context, + controller: localPortController, + inputFormatters: portInputFormatter), + const SizedBox( + width: _kColumn1Width, child: Icon(Icons.arrow_forward_sharp)), + buildTunnelInputCell(context, + controller: remoteHostController, hint: 'localhost'), + buildTunnelInputCell(context, + controller: remotePortController, + inputFormatters: portInputFormatter), + ElevatedButton( + onPressed: () async { + int? localPort = int.tryParse(localPortController.text); + int? remotePort = int.tryParse(remotePortController.text); + if (localPort != null && + remotePort != null && + (remoteHostController.text.isEmpty || + remoteHostController.text.trim().isNotEmpty)) { + await bind.sessionAddPortForward( + sessionId: _ffi.sessionId, + localPort: localPort, + remoteHost: remoteHostController.text.trim().isEmpty + ? 'localhost' + : remoteHostController.text.trim(), + remotePort: remotePort); + localPortController.clear(); + remoteHostController.clear(); + remotePortController.clear(); + refreshTunnelConfig(); + } + }, + child: Text( + translate('Add'), + ), + ).marginSymmetric(horizontal: 10), + ]), + ); + } + + buildTunnelInputCell(BuildContext context, + {required TextEditingController controller, + List? inputFormatters, + String? hint}) { + return Expanded( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: TextField( + controller: controller, + inputFormatters: inputFormatters, + decoration: InputDecoration( + hintText: hint, + )).workaroundFreezeLinuxMint()), + ); + } + + Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) { + text(String label) => Expanded( + child: Text(label, style: const TextStyle(fontSize: 20)) + .marginOnly(left: _kTextLeftMargin)); + + return Container( + height: _kRowHeight, + decoration: BoxDecoration( + color: index % 2 == 0 + ? MyTheme.currentThemeMode() == ThemeMode.dark + ? const Color(0xFF202020) + : const Color(0xFFF4F5F6) + : Theme.of(context).colorScheme.background), + child: Row(children: [ + text(pf.localPort.toString()), + const SizedBox(width: _kColumn1Width), + text(pf.remoteHost), + text(pf.remotePort.toString()), + SizedBox( + width: _kColumn4Width, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + await bind.sessionRemovePortForward( + sessionId: _ffi.sessionId, localPort: pf.localPort); + refreshTunnelConfig(); + }, + ), + ), + ]), + ); + } + + void refreshTunnelConfig() async { + String peer = bind.mainGetPeerSync(id: widget.id); + Map config = jsonDecode(peer); + List infos = config['port_forwards'] as List; + List<_PortForward> result = List.empty(growable: true); + for (var e in infos) { + result.add(_PortForward.fromJson(e)); + } + pfs.value = result; + } + + buildRdp(BuildContext context) { + text1(String label) => Expanded( + child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); + text2(String label) => Expanded( + child: Text( + label, + style: const TextStyle(fontSize: 20), + ).marginOnly(left: _kTextLeftMargin)); + return Theme( + data: Theme.of(context) + .copyWith(colorScheme: Theme.of(context).colorScheme), + child: ListView.builder( + controller: ScrollController(), + itemCount: 2, + itemBuilder: ((context, index) { + if (index == 0) { + return Container( + height: 25, + color: Theme.of(context).scaffoldBackgroundColor, + child: Row(children: [ + text1('Local Port'), + const SizedBox(width: _kColumn1Width), + text1('Remote Host'), + text1('Remote Port'), + ]), + ); + } else { + return Container( + height: _kRowHeight, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background), + child: Row(children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: SizedBox( + width: 120, + child: ElevatedButton( + onPressed: () => + bind.sessionNewRdp(sessionId: _ffi.sessionId), + child: Text( + translate('New RDP'), + ), + ).marginSymmetric(vertical: 10), + ).marginOnly(left: 20), + ), + ), + const SizedBox( + width: _kColumn1Width, + child: Icon(Icons.arrow_forward_sharp)), + text2('localhost'), + text2('RDP'), + ]), + ); + } + })), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/port_forward_tab_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/port_forward_tab_page.dart new file mode 100644 index 0000000..9d366bc --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -0,0 +1,149 @@ +import 'dart:convert'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/pages/port_forward_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +class PortForwardTabPage extends StatefulWidget { + final Map params; + + const PortForwardTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _PortForwardTabPageState(params); +} + +class _PortForwardTabPageState extends State { + late final DesktopTabController tabController; + late final bool isRDP; + + static const IconData selectedIcon = Icons.forward_sharp; + static const IconData unselectedIcon = Icons.forward_outlined; + + _PortForwardTabPageState(Map params) { + isRDP = params['isRDP']; + tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.portForward)); + tabController.onSelected = (id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; + tabController.onRemoved = (_, id) => onRemoveId(id); + tabController.add(TabInfo( + key: params['id'], + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: PortForwardPage( + key: ValueKey(params['id']), + id: params['id'], + password: params['password'], + isSharedPassword: params['isSharedPassword'], + tabController: tabController, + isRDP: isRDP, + forceRelay: params['forceRelay'], + connToken: params['connToken'], + ))); + } + + @override + void initState() { + super.initState(); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + debugPrint( + "[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + // for simplify, just replace connectionId + if (call.method == kWindowEventNewPortForward) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final isRDP = args['isRDP']; + windowOnTop(windowId()); + if (tabController.state.value.tabs.indexWhere((e) => e.key == id) >= + 0) { + debugPrint("port forward $id exists"); + return; + } + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: PortForwardPage( + key: ValueKey(args['id']), + id: id, + password: args['password'], + isSharedPassword: args['isSharedPassword'], + isRDP: isRDP, + tabController: tabController, + forceRelay: args['forceRelay'], + connToken: args['connToken'], + ))); + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } + }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.PortForward, windowId: windowId()); + }); + } + + @override + Widget build(BuildContext context) { + final child = Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: () async { + tabController.clear(); + return true; + }, + tail: AddButton(), + selectedBorderColor: MyTheme.accent, + labelGetter: DesktopTab.tablabelGetter, + ), + ); + final tabWidget = isLinux + ? buildVirtualWindowFrame( + context, + Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: child), + ) + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + )); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : Obx( + () => SubWindowDragToResizeArea( + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, + windowId: stateGlobal.windowId, + ), + ); + } + + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/remote_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/remote_page.dart new file mode 100644 index 0000000..29e710b --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/remote_page.dart @@ -0,0 +1,1054 @@ +import 'dart:async'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_hbb/models/state_model.dart'; + +import '../../consts.dart'; +import '../../common/widgets/overlay.dart'; +import '../../common/widgets/remote_input.dart'; +import '../../common.dart'; +import '../../common/widgets/dialog.dart'; +import '../../common/widgets/toolbar.dart'; +import '../../models/model.dart'; +import '../../models/input_model.dart'; +import '../../models/platform_model.dart'; +import '../../common/shared_state.dart'; +import '../../utils/image.dart'; +import '../widgets/remote_toolbar.dart'; +import '../widgets/kb_layout_type_chooser.dart'; +import '../widgets/tabbar_widget.dart'; + +import 'package:flutter_hbb/native/custom_cursor.dart' + if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart'; + +final SimpleWrapper _firstEnterImage = SimpleWrapper(false); + +// Used to skip session close if "move to new window" is clicked. +final Map closeSessionOnDispose = {}; + +class RemotePage extends StatefulWidget { + RemotePage({ + Key? key, + required this.id, + required this.toolbarState, + this.sessionId, + this.tabWindowId, + this.password, + this.display, + this.displays, + this.tabController, + this.switchUuid, + this.forceRelay, + this.isSharedPassword, + }) : super(key: key) { + initSharedStates(id); + } + + final String id; + final SessionID? sessionId; + final int? tabWindowId; + final int? display; + final List? displays; + final String? password; + final ToolbarState toolbarState; + final String? switchUuid; + final bool? forceRelay; + final bool? isSharedPassword; + final SimpleWrapper?> _lastState = SimpleWrapper(null); + final DesktopTabController? tabController; + + FFI get ffi => (_lastState.value! as _RemotePageState)._ffi; + + @override + State createState() { + final state = _RemotePageState(id); + _lastState.value = state; + return state; + } +} + +class _RemotePageState extends State + with + AutomaticKeepAliveClientMixin, + MultiWindowListener, + TickerProviderStateMixin { + Timer? _timer; + String keyboardMode = "legacy"; + bool _isWindowBlur = false; + final _cursorOverImage = false.obs; + late RxBool _showRemoteCursor; + late RxBool _zoomCursor; + late RxBool _remoteCursorMoved; + late RxBool _keyboardEnabled; + final _uniqueKey = UniqueKey(); + + var _blockableOverlayState = BlockableOverlayState(); + + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); + + // Debounce timer for pointer lock center updates during window events. + // Uses kDefaultPointerLockCenterThrottleMs from consts.dart for the duration. + Timer? _pointerLockCenterDebounceTimer; + + // We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar` + // to identify the toolbar instance and its callback function. + int? _instanceIdOnEnterOrLeaveImage4Toolbar; + Function(bool)? _onEnterOrLeaveImage4Toolbar; + + late FFI _ffi; + + SessionID get sessionId => _ffi.sessionId; + + _RemotePageState(String id) { + _initStates(id); + } + + void _initStates(String id) { + _zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor); + _showRemoteCursor = ShowRemoteCursorState.find(id); + _keyboardEnabled = KeyboardEnabledState.find(id); + _remoteCursorMoved = RemoteCursorMovedState.find(id); + } + + @override + void initState() { + super.initState(); + _ffi = FFI(widget.sessionId); + Get.put(_ffi, tag: widget.id); + _ffi.imageModel.addCallbackOnFirstImage((String peerId) { + _ffi.canvasModel.activateLocalCursor(); + showKBLayoutTypeChooserIfNeeded( + _ffi.ffiModel.pi.platform, _ffi.dialogManager); + _ffi.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); + }); + _ffi.canvasModel.initializeEdgeScrollFallback(this); + _ffi.start( + widget.id, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + switchUuid: widget.switchUuid, + forceRelay: widget.forceRelay, + tabWindowId: widget.tabWindowId, + display: widget.display, + displays: widget.displays, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + WakelockManager.enable(_uniqueKey); + + _ffi.ffiModel.updateEventListener(sessionId, widget.id); + if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); + _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); + _ffi.dialogManager.loadMobileActionsOverlayVisible(); + WidgetsBinding.instance.addPostFrameCallback((_) { + // Session option should be set after models.dart/FFI.start + _showRemoteCursor.value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: 'show-remote-cursor'); + _zoomCursor.value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: kOptionZoomCursor); + }); + DesktopMultiWindow.addListener(this); + // if (!_isCustomCursorInited) { + // customCursorController.registerNeedUpdateCursorCallback( + // (String? lastKey, String? currentKey) async { + // if (_firstEnterImage.value) { + // _firstEnterImage.value = false; + // return true; + // } + // return lastKey == null || lastKey != currentKey; + // }); + // _isCustomCursorInited = true; + // } + + _blockableOverlayState.applyFfi(_ffi); + // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.tabController?.onSelected?.call(widget.id); + }); + + // Register callback to cancel debounce timer when relative mouse mode is disabled + _ffi.inputModel.onRelativeMouseModeDisabled = + _cancelPointerLockCenterDebounceTimer; + } + + /// Cancel the pointer lock center debounce timer + void _cancelPointerLockCenterDebounceTimer() { + _pointerLockCenterDebounceTimer?.cancel(); + _pointerLockCenterDebounceTimer = null; + } + + @override + void onWindowBlur() { + super.onWindowBlur(); + // On windows, we use `focus` way to handle keyboard better. + // Now on Linux, there's some rdev issues which will break the input. + // We disable the `focus` way for non-Windows temporarily. + if (isWindows) { + _isWindowBlur = true; + // unfocus the primary-focus when the whole window is lost focus, + // and let OS to handle events instead. + _rawKeyFocusNode.unfocus(); + } + stateGlobal.isFocused.value = false; + + // When window loses focus, temporarily release relative mouse mode constraints + // to allow user to interact with other applications normally. + // The cursor will be re-hidden and re-centered when window regains focus. + if (_ffi.inputModel.relativeMouseMode.value) { + _ffi.inputModel.onWindowBlur(); + } + } + + @override + void onWindowFocus() { + super.onWindowFocus(); + // See [onWindowBlur]. + if (isWindows) { + _isWindowBlur = false; + } + stateGlobal.isFocused.value = true; + + // Restore relative mouse mode constraints when window regains focus. + if (_ffi.inputModel.relativeMouseMode.value) { + _rawKeyFocusNode.requestFocus(); + _ffi.inputModel.onWindowFocus(); + } + } + + @override + void onWindowRestore() { + super.onWindowRestore(); + // On windows, we use `onWindowRestore` way to handle window restore from + // a minimized state. + if (isWindows) { + _isWindowBlur = false; + } + WakelockManager.enable(_uniqueKey); + // Update pointer lock center when window is restored + _updatePointerLockCenterIfNeeded(); + } + + // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not. + @override + void onWindowMaximize() { + super.onWindowMaximize(); + WakelockManager.enable(_uniqueKey); + // Update pointer lock center when window is maximized + _updatePointerLockCenterIfNeeded(); + } + + @override + void onWindowResize() { + super.onWindowResize(); + // Update pointer lock center when window is resized + _updatePointerLockCenterIfNeeded(); + } + + @override + void onWindowMove() { + super.onWindowMove(); + // Update pointer lock center when window is moved + _updatePointerLockCenterIfNeeded(); + } + + /// Update pointer lock center with debouncing to avoid excessive updates + /// during rapid window move/resize events. + void _updatePointerLockCenterIfNeeded() { + if (!_ffi.inputModel.relativeMouseMode.value) return; + + // Cancel any pending update and schedule a new one (debounce pattern) + _pointerLockCenterDebounceTimer?.cancel(); + _pointerLockCenterDebounceTimer = Timer( + const Duration(milliseconds: kDefaultPointerLockCenterThrottleMs), + () { + if (!mounted) return; + if (_ffi.inputModel.relativeMouseMode.value) { + _ffi.inputModel.updatePointerLockCenter(); + } + }, + ); + } + + @override + void onWindowMinimize() { + super.onWindowMinimize(); + WakelockManager.disable(_uniqueKey); + // Release cursor constraints when minimized + if (_ffi.inputModel.relativeMouseMode.value) { + _ffi.inputModel.onWindowBlur(); + } + } + + @override + void onWindowEnterFullScreen() { + super.onWindowEnterFullScreen(); + if (isMacOS) { + stateGlobal.setFullscreen(true); + } + } + + @override + void onWindowLeaveFullScreen() { + super.onWindowLeaveFullScreen(); + if (isMacOS) { + stateGlobal.setFullscreen(false); + } + } + + @override + Future dispose() async { + final closeSession = closeSessionOnDispose.remove(widget.id) ?? true; + + // https://github.com/flutter/flutter/issues/64935 + super.dispose(); + debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); + + // Defensive cleanup: ensure host system-key propagation is reset even if + // MouseRegion.onExit never fired (e.g., tab closed while cursor inside). + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true); + + _pointerLockCenterDebounceTimer?.cancel(); + _pointerLockCenterDebounceTimer = null; + // Clear callback reference to prevent memory leaks and stale references + _ffi.inputModel.onRelativeMouseModeDisabled = null; + // Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...). + _ffi.textureModel.onRemotePageDispose(closeSession); + if (closeSession) { + // ensure we leave this session, this is a double check + _ffi.inputModel.enterOrLeave(false); + } + DesktopMultiWindow.removeListener(this); + _ffi.dialogManager.hideMobileActionsOverlay(); + _ffi.imageModel.disposeImage(); + _ffi.cursorModel.disposeImages(); + _rawKeyFocusNode.dispose(); + await _ffi.close(closeSession: closeSession); + _timer?.cancel(); + _ffi.dialogManager.dismissAll(); + if (closeSession) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + } + WakelockManager.disable(_uniqueKey); + await Get.delete(tag: widget.id); + removeSharedStates(widget.id); + } + + Widget emptyOverlay() => BlockableOverlay( + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + state: _blockableOverlayState, + underlying: Container( + color: Colors.transparent, + ), + ); + + Widget buildBody(BuildContext context) { + remoteToolbar(BuildContext context) => RemoteToolbar( + id: widget.id, + ffi: _ffi, + state: widget.toolbarState, + onEnterOrLeaveImageSetter: (id, func) { + _instanceIdOnEnterOrLeaveImage4Toolbar = id; + _onEnterOrLeaveImage4Toolbar = func; + }, + onEnterOrLeaveImageCleaner: (id) { + // If _instanceIdOnEnterOrLeaveImage4Toolbar != id + // it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar. + if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) { + _instanceIdOnEnterOrLeaveImage4Toolbar = null; + _onEnterOrLeaveImage4Toolbar = null; + } + }, + setRemoteState: setState, + ); + + bodyWidget() { + return Stack( + children: [ + Container( + color: kColorCanvas, + child: RawKeyFocusScope( + focusNode: _rawKeyFocusNode, + onFocusChange: (bool imageFocused) { + debugPrint( + "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); + // See [onWindowBlur]. + if (isWindows) { + if (_isWindowBlur) { + imageFocused = false; + Future.delayed(Duration.zero, () { + _rawKeyFocusNode.unfocus(); + }); + } + if (imageFocused) { + _ffi.inputModel.enterOrLeave(true); + } else { + _ffi.inputModel.enterOrLeave(false); + } + } + }, + inputModel: _ffi.inputModel, + child: getBodyForDesktop(context))), + Stack( + children: [ + _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isTrue + ? emptyOverlay() + : () { + if (!_ffi.ffiModel.isPeerAndroid) { + return Offstage(); + } else { + return Obx(() => Offstage( + offstage: _ffi.dialogManager + .mobileActionsOverlayVisible.isFalse, + child: Overlay(initialEntries: [ + makeMobileActionsOverlayEntry( + () => _ffi.dialogManager + .setMobileActionsOverlayVisible(false), + ffi: _ffi, + ) + ]), + )); + } + }(), + // Use Overlay to enable rebuild every time on menu button click. + // Hide toolbar when relative mouse mode is active to prevent + // cursor from escaping to toolbar area. + Obx(() => _ffi.inputModel.relativeMouseMode.value + ? const Offstage() + : _ffi.ffiModel.pi.isSet.isTrue + ? Overlay(initialEntries: [ + OverlayEntry(builder: remoteToolbar) + ]) + : remoteToolbar(context)), + _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(), + ], + ), + ], + ); + } + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: Obx(() { + final imageReady = _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isFalse; + if (imageReady) { + // If the privacy mode(disable physical displays) is switched, + // we should not dismiss the dialog immediately. + if (DateTime.now().difference(togglePrivacyModeTime) > + const Duration(milliseconds: 3000)) { + // `dismissAll()` is to ensure that the state is clean. + // It's ok to call dismissAll() here. + _ffi.dialogManager.dismissAll(); + // Recreate the block state to refresh the state. + _blockableOverlayState = BlockableOverlayState(); + _blockableOverlayState.applyFfi(_ffi); + } + // Block the whole `bodyWidget()` when dialog shows. + return BlockableOverlay( + underlying: bodyWidget(), + state: _blockableOverlayState, + ); + } else { + // `_blockableOverlayState` is not recreated here. + // The toolbar's block state won't work properly when reconnecting, but that's okay. + return bodyWidget(); + } + }), + ); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return WillPopScope( + onWillPop: () async { + clientClose(sessionId, _ffi); + 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), + ChangeNotifierProvider.value(value: _ffi.recordingModel), + ], child: buildBody(context))); + } + + void enterView(PointerEnterEvent evt) { + _ffi.canvasModel.rearmEdgeScroll(); + + _cursorOverImage.value = true; + _firstEnterImage.value = true; + if (_onEnterOrLeaveImage4Toolbar != null) { + try { + _onEnterOrLeaveImage4Toolbar!(true); + } catch (e) { + // + } + } + + // See [onWindowBlur]. + if (!isWindows) { + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + _ffi.inputModel.enterOrLeave(true); + } + } + + void leaveView(PointerExitEvent evt) { + _ffi.canvasModel.disableEdgeScroll(); + + if (_ffi.ffiModel.keyboard) { + _ffi.inputModel.tryMoveEdgeOnExit(evt.position); + } + + _cursorOverImage.value = false; + _firstEnterImage.value = false; + if (_onEnterOrLeaveImage4Toolbar != null) { + try { + _onEnterOrLeaveImage4Toolbar!(false); + } catch (e) { + // + } + } + + // See [onWindowBlur]. + if (!isWindows) { + _ffi.inputModel.enterOrLeave(false); + } + } + + Widget _buildRawTouchAndPointerRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return RawTouchGestureDetectorRegion( + child: _buildRawPointerMouseRegion(child, onEnter, onExit), + ffi: _ffi, + ); + } + + Widget _buildRawPointerMouseRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return RawPointerMouseRegion( + onEnter: onEnter, + onExit: onExit, + onPointerDown: (event) { + // A double check for blur status. + // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. + // Sometimes the system does not send the necessary focus event to flutter. We should manually + // handle this inconsistent status by setting `_isWindowBlur` to false. So we can + // ensure the grab-key thread is running when our users are clicking the remote canvas. + if (_isWindowBlur) { + debugPrint( + "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); + _isWindowBlur = false; + } + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + }, + inputModel: _ffi.inputModel, + child: child, + ); + } + + Widget getBodyForDesktop(BuildContext context) { + var paints = [ + MouseRegion( + onEnter: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false); + }, + onExit: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true); + }, + child: _ViewStyleUpdater( + canvasModel: _ffi.canvasModel, + inputModel: _ffi.inputModel, + child: Builder(builder: (context) { + final peerDisplay = CurrentDisplayState.find(widget.id); + return Obx( + () => _ffi.ffiModel.pi.isSet.isFalse + ? Container(color: Colors.transparent) + : Obx(() { + _ffi.textureModel.updateCurrentDisplay(peerDisplay.value); + return ImagePaint( + id: widget.id, + zoomCursor: _zoomCursor, + cursorOverImage: _cursorOverImage, + keyboardEnabled: _keyboardEnabled, + remoteCursorMoved: _remoteCursorMoved, + listenerBuilder: (child) => + _buildRawTouchAndPointerRegion( + child, enterView, leaveView), + ffi: _ffi, + ); + }), + ); + }), + ), + ) + ]; + + if (!_ffi.canvasModel.cursorEmbedded) { + paints + .add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse + ? Offstage() + : CursorPaint( + id: widget.id, + zoomCursor: _zoomCursor, + ))); + } + paints.add( + Positioned( + top: 10, + right: 10, + child: _buildRawTouchAndPointerRegion( + QualityMonitor(_ffi.qualityMonitorModel), null, null), + ), + ); + return Stack( + children: paints, + ); + } + + @override + bool get wantKeepAlive => true; +} + +/// A widget that tracks the view size and updates CanvasModel.updateViewStyle() +/// and InputModel.updateImageWidgetSize() only when size actually changes. +/// This avoids scheduling post-frame callbacks on every LayoutBuilder rebuild. +class _ViewStyleUpdater extends StatefulWidget { + final CanvasModel canvasModel; + final InputModel inputModel; + final Widget child; + + const _ViewStyleUpdater({ + Key? key, + required this.canvasModel, + required this.inputModel, + required this.child, + }) : super(key: key); + + @override + State<_ViewStyleUpdater> createState() => _ViewStyleUpdaterState(); +} + +class _ViewStyleUpdaterState extends State<_ViewStyleUpdater> { + Size? _lastSize; + bool _callbackScheduled = false; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + final maxHeight = constraints.maxHeight; + // Guard against infinite constraints (e.g., unconstrained ancestor). + if (!maxWidth.isFinite || !maxHeight.isFinite) { + return widget.child; + } + final newSize = Size(maxWidth, maxHeight); + if (_lastSize != newSize) { + _lastSize = newSize; + // Schedule the update for after the current frame to avoid setState during build. + // Use _callbackScheduled flag to prevent accumulating multiple callbacks + // when size changes rapidly before any callback executes. + if (!_callbackScheduled) { + _callbackScheduled = true; + SchedulerBinding.instance.addPostFrameCallback((_) { + _callbackScheduled = false; + final currentSize = _lastSize; + if (mounted && currentSize != null) { + widget.canvasModel.updateViewStyle(); + widget.inputModel.updateImageWidgetSize(currentSize); + } + }); + } + } + return widget.child; + }, + ); + } +} + +class ImagePaint extends StatefulWidget { + final FFI ffi; + final String id; + final RxBool zoomCursor; + final RxBool cursorOverImage; + final RxBool keyboardEnabled; + final RxBool remoteCursorMoved; + final Widget Function(Widget)? listenerBuilder; + + ImagePaint( + {Key? key, + required this.ffi, + required this.id, + required this.zoomCursor, + required this.cursorOverImage, + required this.keyboardEnabled, + required this.remoteCursorMoved, + this.listenerBuilder}) + : super(key: key); + + @override + State createState() => _ImagePaintState(); +} + +class _ImagePaintState extends State { + bool _lastRemoteCursorMoved = false; + + String get id => widget.id; + RxBool get zoomCursor => widget.zoomCursor; + RxBool get cursorOverImage => widget.cursorOverImage; + RxBool get keyboardEnabled => widget.keyboardEnabled; + RxBool get remoteCursorMoved => widget.remoteCursorMoved; + Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; + + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + var c = Provider.of(context); + final s = c.scale; + + bool isViewAdaptive() => c.viewStyle.style == kRemoteViewStyleAdaptive; + bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal; + + mouseRegion({child}) => Obx(() { + double getCursorScale() { + var c = Provider.of(context); + var cursorScale = 1.0; + if (isWindows) { + // debug win10 + if (zoomCursor.value && isViewAdaptive()) { + cursorScale = s * c.devicePixelRatio; + } + } else { + if (zoomCursor.value || isViewOriginal()) { + cursorScale = s; + } + } + return cursorScale; + } + + return MouseRegion( + cursor: cursorOverImage.isTrue + ? c.cursorEmbedded + ? SystemMouseCursors.none + // Hide cursor when relative mouse mode is active + : widget.ffi.inputModel.relativeMouseMode.value + ? SystemMouseCursors.none + : keyboardEnabled.isTrue + ? (() { + if (remoteCursorMoved.isTrue) { + _lastRemoteCursorMoved = true; + return SystemMouseCursors.none; + } else { + if (_lastRemoteCursorMoved) { + _lastRemoteCursorMoved = false; + _firstEnterImage.value = true; + } + return _buildCustomCursor( + context, getCursorScale()); + } + }()) + : _buildDisabledCursor(context, getCursorScale()) + : MouseCursor.defer, + onHover: (evt) {}, + child: child); + }); + if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) { + final paintWidth = c.getDisplayWidth() * s; + final paintHeight = c.getDisplayHeight() * s; + final paintSize = Size(paintWidth, paintHeight); + final paintWidget = + m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender + ? _BuildPaintTextureRender( + c, s, Offset.zero, paintSize, isViewOriginal()) + : _buildScrollbarNonTextureRender(m, paintSize, s); + return NotificationListener( + onNotification: (notification) { + c.updateScrollPercent(); + return false; + }, + child: mouseRegion( + child: Obx(() => _buildCrossScrollbarFromLayout( + context, + _buildListener(paintWidget), + c.size, + paintSize, + c.scrollHorizontal, + c.scrollVertical, + )), + )); + } else { + if (c.size.width > 0 && c.size.height > 0) { + final paintWidget = + m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender + ? _BuildPaintTextureRender( + c, + s, + Offset( + isLinux ? c.x.toInt().toDouble() : c.x, + isLinux ? c.y.toInt().toDouble() : c.y, + ), + c.size, + isViewOriginal()) + : _buildScrollAutoNonTextureRender(m, c, s); + return mouseRegion(child: _buildListener(paintWidget)); + } else { + return Container(); + } + } + } + + Widget _buildScrollbarNonTextureRender( + ImageModel m, Size imageSize, double s) { + return CustomPaint( + size: imageSize, + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + ); + } + + Widget _buildScrollAutoNonTextureRender( + ImageModel m, CanvasModel c, double s) { + double sizeScale = s; + if (widget.ffi.ffiModel.isPeerLinux) { + final displays = widget.ffi.ffiModel.pi.getCurDisplays(); + if (displays.isNotEmpty) { + sizeScale = s / displays[0].scale; + } + } + return CustomPaint( + size: Size(c.size.width, c.size.height), + painter: ImagePainter( + image: m.image, + x: c.x / sizeScale, + y: c.y / sizeScale, + scale: sizeScale), + ); + } + + Widget _BuildPaintTextureRender( + CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) { + final ffiModel = c.parent.target!.ffiModel; + final displays = ffiModel.pi.getCurDisplays(); + final children = []; + final rect = ffiModel.rect; + if (rect == null) { + return Container(); + } + final isPeerLinux = ffiModel.isPeerLinux; + final curDisplay = ffiModel.pi.currentDisplay; + for (var i = 0; i < displays.length; i++) { + final textureId = widget.ffi.textureModel + .getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay); + if (true) { + // both "textureId.value != -1" and "true" seems ok + final sizeScale = isPeerLinux ? s / displays[i].scale : s; + children.add(Positioned( + left: (displays[i].x - rect.left) * s + offset.dx, + top: (displays[i].y - rect.top) * s + offset.dy, + width: displays[i].width * sizeScale, + height: displays[i].height * sizeScale, + child: Obx(() => Texture( + textureId: textureId.value, + filterQuality: + isViewOriginal ? FilterQuality.none : FilterQuality.low, + )), + )); + } + } + return SizedBox( + width: size.width, + height: size.height, + child: Stack(children: children), + ); + } + + MouseCursor _buildCustomCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = cursor.cache ?? preDefaultCursor.cache; + return buildCursorOfCache(cursor, scale, cache); + } + + MouseCursor _buildDisabledCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = preForbiddenCursor.cache; + return buildCursorOfCache(cursor, scale, cache); + } + + Widget _buildCrossScrollbarFromLayout( + BuildContext context, + Widget child, + Size layoutSize, + Size size, + ScrollController horizontal, + ScrollController vertical, + ) { + var widget = child; + if (layoutSize.width < size.width) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: horizontal, + scrollDirection: Axis.horizontal, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Row( + children: [ + Container( + width: ((layoutSize.width - size.width) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.height < size.height) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: vertical, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Column( + children: [ + Container( + height: ((layoutSize.height - size.height) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.width < size.width) { + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: horizontal, + thumbVisibility: false, + trackVisibility: false, + notificationPredicate: layoutSize.height < size.height + ? (notification) => notification.depth == 1 + : defaultScrollNotificationPredicate, + child: widget, + ); + } + if (layoutSize.height < size.height) { + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: vertical, + thumbVisibility: false, + trackVisibility: false, + child: widget, + ); + } + + return Container( + child: widget, + width: layoutSize.width, + height: layoutSize.height, + ); + } + + Widget _buildListener(Widget child) { + if (listenerBuilder != null) { + return listenerBuilder!(child); + } else { + return child; + } + } +} + +class CursorPaint extends StatelessWidget { + final String id; + final RxBool zoomCursor; + + const CursorPaint({ + Key? key, + required this.id, + required this.zoomCursor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + final c = Provider.of(context); + double hotx = m.hotx; + double hoty = m.hoty; + if (m.image == null) { + if (preDefaultCursor.image != null) { + hotx = preDefaultCursor.image!.width / 2; + hoty = preDefaultCursor.image!.height / 2; + } + } + + double cx = c.x; + double cy = c.y; + if (c.viewStyle.style == kRemoteViewStyleOriginal && + c.scrollStyle == ScrollStyle.scrollbar) { + final rect = c.parent.target!.ffiModel.rect; + if (rect == null) { + // unreachable! + debugPrint('unreachable! The displays rect is null.'); + return Container(); + } + if (cx < 0) { + final imageWidth = rect.width * c.scale; + cx = -imageWidth * c.scrollX; + } + if (cy < 0) { + final imageHeight = rect.height * c.scale; + cy = -imageHeight * c.scrollY; + } + } + + double x = (m.x - hotx) * c.scale + cx; + double y = (m.y - hoty) * c.scale + cy; + double scale = 1.0; + final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; + if (zoomCursor.value || isViewOriginal) { + x = m.x - hotx + cx / c.scale; + y = m.y - hoty + cy / c.scale; + scale = c.scale; + } + + return CustomPaint( + painter: ImagePainter( + image: m.image ?? preDefaultCursor.image, + x: x, + y: y, + scale: scale, + ), + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/remote_tab_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/remote_tab_page.dart new file mode 100644 index 0000000..ccd5935 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/remote_tab_page.dart @@ -0,0 +1,624 @@ +import 'dart:convert'; +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/input_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/pages/remote_page.dart'; +import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:bot_toast/bot_toast.dart'; + +import '../../common/widgets/dialog.dart'; +import '../../models/platform_model.dart'; + +class _MenuTheme { + static const Color blueColor = MyTheme.button; + // kMinInteractiveDimension + static const double height = 20.0; + static const double dividerHeight = 12.0; +} + +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 { + final tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.remoteScreen)); + final contentKey = UniqueKey(); + static const IconData selectedIcon = Icons.desktop_windows_sharp; + static const IconData unselectedIcon = Icons.desktop_windows_outlined; + + String? peerId; + bool _isScreenRectSet = false; + int? _display; + + var connectionMap = RxList.empty(growable: true); + + _ConnectionTabPageState(Map params) { + RemoteCountState.init(); + peerId = params['id']; + final sessionId = params['session_id']; + final tabWindowId = params['tab_window_id']; + final display = params['display']; + final displays = params['displays']; + final screenRect = parseParamScreenRect(params); + _isScreenRectSet = screenRect != null; + _display = display as int?; + tryMoveToScreenAndSetFullscreen(screenRect); + if (peerId != null) { + ConnectionTypeState.init(peerId!); + tabController.onSelected = (id) { + final remotePage = tabController.widget(id); + if (remotePage is RemotePage) { + final ffi = remotePage.ffi; + bind.setCurSessionId(sessionId: ffi.sessionId); + } + WindowController.fromWindowId(params['windowId']) + .setTitle(getWindowNameWithId(id)); + UnreadChatCountState.find(id).value = 0; + }; + tabController.add(TabInfo( + key: peerId!, + label: peerId!, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () async { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: peerId!, + tabController: tabController, + )) { + return; + } + tabController.closeBy(peerId!); + }, + page: RemotePage( + key: ValueKey(peerId), + id: peerId!, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: params['password'], + toolbarState: ToolbarState(), + tabController: tabController, + switchUuid: params['switch_uuid'], + forceRelay: params['forceRelay'], + isSharedPassword: params['isSharedPassword'], + ), + )); + _update_remote_count(); + } + tabController.onRemoved = (_, id) => onRemoveId(id); + rustDeskWinManager.setMethodHandler(_remoteMethodHandler); + } + + @override + void initState() { + super.initState(); + + if (!_isScreenRectSet) { + Future.delayed(Duration.zero, () { + restoreWindowPosition( + WindowType.RemoteDesktop, + windowId: windowId(), + peerId: tabController.state.value.tabs.isEmpty + ? null + : tabController.state.value.tabs[0].key, + display: _display, + ); + }); + } + } + + @override + Widget build(BuildContext context) { + final child = Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _RelativeMouseModeHint(tabController: tabController), + const AddButton(), + ], + ), + selectedBorderColor: MyTheme.accent, + pageViewBuilder: (pageView) => pageView, + labelGetter: DesktopTab.tablabelGetter, + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + bool secure = + connectionType.secure.value == ConnectionType.strSecure; + bool direct = + connectionType.direct.value == ConnectionType.strDirect; + String msgConn = getConnectionText( + secure, direct, connectionType.stream_type.value); + var msgFingerprint = '${translate('Fingerprint')}:\n'; + var fingerprint = FingerprintState.find(key).value; + if (fingerprint.isEmpty) { + fingerprint = 'N/A'; + } + if (fingerprint.length > 5 * 8) { + var first = fingerprint.substring(0, 39); + var second = fingerprint.substring(40); + msgFingerprint += '$first\n$second'; + } else { + msgFingerprint += fingerprint; + } + + final tab = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgConn\n$msgFingerprint', + child: SvgPicture.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + unreadMessageCountBuilder(UnreadChatCountState.find(key)) + .marginOnly(left: 4), + ], + ); + + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + final remotePage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as RemotePage; + if (remotePage.ffi.ffiModel.pi.isSet.isTrue && e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return _tabMenuBuilder(key, cancelFunc); + }, + target: e.position, + ); + } + }, + child: tab, + ); + } + }), + ), + ); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : workaroundWindowBorder( + context, + Obx(() => Container( + decoration: BoxDecoration( + border: Border.all( + color: MyTheme.color(context).border!, + width: stateGlobal.windowBorderWidth.value), + ), + child: child, + ))); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : Obx(() => SubWindowDragToResizeArea( + key: contentKey, + child: tabWidget, + // Specially configured for a better resize area and remote control. + childPadding: kDragToResizeAreaPadding, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, + windowId: stateGlobal.windowId, + )); + } + + // Note: Some dup code to ../widgets/remote_toolbar + Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) { + final List> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + final remotePage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as RemotePage; + final ffi = remotePage.ffi; + final pi = ffi.ffiModel.pi; + final perms = ffi.ffiModel.permissions; + final sessionId = ffi.sessionId; + final toolbarState = remotePage.toolbarState; + menu.addAll([ + MenuEntryButton( + childBuilder: (TextStyle? style) => Obx(() => Text( + translate( + toolbarState.hide.isTrue ? 'Show Toolbar' : 'Hide Toolbar'), + style: style, + )), + proc: () { + toolbarState.switchHide(sessionId); + cancelFunc(); + }, + padding: padding, + ), + ]); + + if (tabController.state.value.tabs.length > 1) { + final splitAction = MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Move tab to new window'), + style: style, + ), + proc: () async { + await DesktopMultiWindow.invokeMethod( + kMainWindowId, + kWindowEventMoveTabToNewWindow, + '${windowId()},$key,$sessionId,RemoteDesktop'); + cancelFunc(); + }, + padding: padding, + ); + menu.insert(1, splitAction); + } + + if (perms['restart'] != false && + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS)) { + menu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Restart remote device'), + style: style, + ), + proc: () => showRestartRemoteDevice( + pi, peerId ?? '', sessionId, ffi.dialogManager), + padding: padding, + dismissOnClicked: true, + dismissCallback: cancelFunc, + )); + } + + if (perms['keyboard'] != false && !ffi.ffiModel.viewOnly) { + menu.add(RemoteMenuEntry.insertLock(sessionId, padding, + dismissFunc: cancelFunc)); + + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { + menu.add(RemoteMenuEntry.insertCtrlAltDel(sessionId, padding, + dismissFunc: cancelFunc)); + } + } + + menu.addAll([ + MenuEntryDivider(), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Copy Fingerprint'), + style: style, + ), + proc: () => onCopyFingerprint(FingerprintState.find(key).value), + padding: padding, + dismissOnClicked: true, + dismissCallback: cancelFunc, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Close'), + style: style, + ), + proc: () async { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: key, + tabController: tabController, + )) { + return; + } + tabController.closeBy(key); + cancelFunc(); + }, + padding: padding, + ) + ]); + + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenuTheme.blueColor, + height: _MenuTheme.height, + dividerHeight: _MenuTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + void onRemoveId(String id) async { + if (tabController.state.value.tabs.isEmpty) { + // Keep calling until the window status is hidden. + // + // Workaround for Windows: + // If you click other buttons and close in msgbox within a very short period of time, the close may fail. + // `await WindowController.fromWindowId(windowId()).close();`. + Future loopCloseWindow() async { + int c = 0; + final windowController = WindowController.fromWindowId(windowId()); + while (c < 20 && + tabController.state.value.tabs.isEmpty && + (!await windowController.isHidden())) { + await windowController.close(); + await Future.delayed(Duration(milliseconds: 100)); + c++; + } + } + + loopCloseWindow(); + } + ConnectionTypeState.delete(id); + // Clean up relative mouse mode state for this peer. + stateGlobal.relativeMouseModeState.remove(id); + _update_remote_count(); + } + + int windowId() { + return widget.params["windowId"]; + } + + Future handleWindowCloseButton() async { + final connLength = tabController.length; + if (connLength == 1) { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: tabController.state.value.tabs[0].key, + tabController: tabController, + )) { + return false; + } + } + if (connLength <= 1) { + tabController.clear(); + return true; + } else { + final bool res; + if (!option2bool(kOptionEnableConfirmClosingTabs, + bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + tabController.clear(); + } + return res; + } + } + + _update_remote_count() => + RemoteCountState.find().value = tabController.length; + + Future _remoteMethodHandler(call, fromWindowId) async { + debugPrint( + "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + + dynamic returnValue; + // for simplify, just replace connectionId + if (call.method == kWindowEventNewRemoteDesktop) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final switchUuid = args['switch_uuid']; + final sessionId = args['session_id']; + final tabWindowId = args['tab_window_id']; + final display = args['display']; + final displays = args['displays']; + final screenRect = parseParamScreenRect(args); + final prePeerCount = tabController.length; + Future.delayed(Duration.zero, () async { + if (stateGlobal.fullscreen.isTrue) { + await WindowController.fromWindowId(windowId()).setFullscreen(false); + stateGlobal.setFullscreen(false, procWnd: false); + } + await setNewConnectWindowFrame(windowId(), id!, prePeerCount, + WindowType.RemoteDesktop, display, screenRect); + Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { + await windowOnTop(windowId()); + }); + }); + ConnectionTypeState.init(id); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () async { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: id, + tabController: tabController, + )) { + return; + } + tabController.closeBy(id); + }, + page: RemotePage( + key: ValueKey(id), + id: id, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: args['password'], + toolbarState: ToolbarState(), + tabController: tabController, + switchUuid: switchUuid, + forceRelay: args['forceRelay'], + isSharedPassword: args['isSharedPassword'], + ), + )); + } else if (call.method == kWindowDisableGrabKeyboard) { + // ??? + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } else if (call.method == kWindowEventActiveSession) { + final jumpOk = tabController.jumpToByKey(call.arguments); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventActiveDisplaySession) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final display = args['display']; + final jumpOk = tabController.jumpToByKeyAndDisplay(id, display); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventGetRemoteList) { + return tabController.state.value.tabs + .map((e) => e.key) + .toList() + .join(','); + } else if (call.method == kWindowEventGetSessionIdList) { + return tabController.state.value.tabs + .map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}') + .toList() + .join(';'); + } else if (call.method == kWindowEventGetCachedSessionData) { + // Ready to show new window and close old tab. + final args = jsonDecode(call.arguments); + final id = args['id']; + final close = args['close']; + try { + final remotePage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == id) + .page as RemotePage; + returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString(); + } catch (e) { + debugPrint('Failed to get cached session data: $e'); + } + if (close && returnValue != null) { + closeSessionOnDispose[id] = false; + tabController.closeBy(id); + } + } else if (call.method == kWindowEventRemoteWindowCoords) { + final remotePage = + tabController.state.value.selectedTabInfo.page as RemotePage; + final ffi = remotePage.ffi; + final displayRect = ffi.ffiModel.displaysRect(); + if (displayRect != null) { + final wc = WindowController.fromWindowId(windowId()); + Rect? frame; + try { + frame = await wc.getFrame(); + } catch (e) { + debugPrint( + "Failed to get frame of window $windowId, it may be hidden"); + } + if (frame != null) { + ffi.cursorModel.moveLocal(0, 0); + final coords = RemoteWindowCoords( + frame, + CanvasCoords.fromCanvasModel(ffi.canvasModel), + CursorCoords.fromCursorModel(ffi.cursorModel), + displayRect); + returnValue = jsonEncode(coords.toJson()); + } + } + } else if (call.method == kWindowEventSetFullscreen) { + stateGlobal.setFullscreen(call.arguments == 'true'); + } + _update_remote_count(); + return returnValue; + } +} + +/// A widget that displays a hint in the tab bar when relative mouse mode is active. +/// This helps users remember how to exit relative mouse mode. +class _RelativeMouseModeHint extends StatelessWidget { + final DesktopTabController tabController; + + const _RelativeMouseModeHint({Key? key, required this.tabController}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + // Check if there are any tabs + if (tabController.state.value.tabs.isEmpty) { + return const SizedBox.shrink(); + } + + // Get current selected tab's RemotePage + final selectedTabInfo = tabController.state.value.selectedTabInfo; + if (selectedTabInfo.page is! RemotePage) { + return const SizedBox.shrink(); + } + + final remotePage = selectedTabInfo.page as RemotePage; + final String peerId = remotePage.id; + + // Use global state to check relative mouse mode (synced from InputModel). + // This avoids timing issues with FFI registration. + final isRelativeMouseMode = + stateGlobal.relativeMouseModeState[peerId] ?? false; + + if (!isRelativeMouseMode) { + return const SizedBox.shrink(); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.2), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.orange.withOpacity(0.5)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.mouse, + size: 14, + color: Colors.orange[700], + ), + const SizedBox(width: 4), + Text( + translate( + 'rel-mouse-exit-{${isMacOS ? "Cmd+G" : "Ctrl+Alt"}}-tip'), + style: TextStyle( + fontSize: 11, + color: Colors.orange[700], + ), + ), + ], + ), + ); + }); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/server_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/server_page.dart new file mode 100644 index 0000000..7d48452 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/server_page.dart @@ -0,0 +1,1415 @@ +// original cm window in Sciter version. + +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/audio_input.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_hbb/models/cm_file_model.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; +import 'package:get/get.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; +import 'package:provider/provider.dart'; +import 'package:window_manager/window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../common.dart'; +import '../../common/widgets/chat_page.dart'; +import '../../models/file_model.dart'; +import '../../models/platform_model.dart'; +import '../../models/server_model.dart'; + +class DesktopServerPage extends StatefulWidget { + const DesktopServerPage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopServerPageState(); +} + +class _DesktopServerPageState extends State + with WindowListener, AutomaticKeepAliveClientMixin { + final tabController = gFFI.serverModel.tabController; + + _DesktopServerPageState() { + gFFI.ffiModel.updateEventListener(gFFI.sessionId, ""); + Get.put(tabController); + tabController.onRemoved = (_, id) { + onRemoveId(id); + }; + } + + @override + void initState() { + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowClose() { + Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) { + if (isMacOS) { + RdPlatformChannel.instance.terminate(); + } else { + windowManager.setPreventClose(false); + windowManager.close(); + } + }); + super.onWindowClose(); + } + + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + windowManager.close(); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.serverModel), + ChangeNotifierProvider.value(value: gFFI.chatModel), + ], + child: Consumer( + builder: (context, serverModel, child) { + final body = Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: ConnectionManager(), + ); + return isLinux + ? buildVirtualWindowFrame(context, body) + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: + Border.all(color: MyTheme.color(context).border!)), + child: body, + )); + }, + ), + ); + } + + @override + bool get wantKeepAlive => true; +} + +class ConnectionManager extends StatefulWidget { + @override + State createState() => ConnectionManagerState(); +} + +class ConnectionManagerState extends State + with WidgetsBindingObserver { + final RxBool _controlPageBlock = false.obs; + final RxBool _sidePageBlock = false.obs; + + ConnectionManagerState() { + gFFI.serverModel.tabController.onSelected = (client_id_str) { + final client_id = int.tryParse(client_id_str); + if (client_id != null) { + final client = + gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == client_id); + if (client != null) { + gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id)); + if (client.unreadChatMessageCount.value > 0) { + WidgetsBinding.instance.addPostFrameCallback((_) { + client.unreadChatMessageCount.value = 0; + gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id)); + }); + } + windowManager.setTitle(getWindowNameWithId(client.peerId)); + gFFI.cmFileModel.updateCurrentClientId(client.id); + } + } + }; + gFFI.chatModel.isConnManager = true; + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if (state == AppLifecycleState.resumed) { + if (!allowRemoteCMModification()) { + shouldBeBlocked(_controlPageBlock, null); + shouldBeBlocked(_sidePageBlock, null); + } + } + } + + @override + void initState() { + gFFI.serverModel.updateClientState(); + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final serverModel = Provider.of(context); + pointerHandler(PointerEvent e) { + if (serverModel.cmHiddenTimer != null) { + serverModel.cmHiddenTimer!.cancel(); + serverModel.cmHiddenTimer = null; + debugPrint("CM hidden timer has been canceled"); + } + } + + return serverModel.clients.isEmpty + ? Column( + children: [ + buildTitleBar(), + Expanded( + child: Center( + child: Text(translate("Waiting")), + ), + ), + ], + ) + : Listener( + onPointerDown: pointerHandler, + onPointerMove: pointerHandler, + child: DesktopTab( + showTitle: false, + showMaximize: false, + showMinimize: true, + showClose: true, + onWindowCloseButton: handleWindowCloseButton, + controller: serverModel.tabController, + selectedBorderColor: MyTheme.accent, + maxLabelWidth: 100, + tail: null, //buildScrollJumper(), + tabBuilder: (key, icon, label, themeConf) { + final client = serverModel.clients + .firstWhereOrNull((client) => client.id.toString() == key); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Tooltip( + message: key, + waitDuration: Duration(seconds: 1), + child: label), + unreadMessageCountBuilder(client?.unreadChatMessageCount) + .marginOnly(left: 4), + ], + ); + }, + pageViewBuilder: (pageView) => LayoutBuilder( + builder: (context, constrains) { + var borderWidth = 0.0; + if (constrains.maxWidth > + kConnectionManagerWindowSizeClosedChat.width) { + borderWidth = kConnectionManagerWindowSizeOpenChat.width - + constrains.maxWidth; + } else { + borderWidth = kConnectionManagerWindowSizeClosedChat.width - + constrains.maxWidth; + } + if (borderWidth < 0 || borderWidth > 50) { + borderWidth = 0; + } + final realClosedWidth = + kConnectionManagerWindowSizeClosedChat.width - + borderWidth; + final realChatPageWidth = + constrains.maxWidth - realClosedWidth; + final row = Row(children: [ + if (constrains.maxWidth > + kConnectionManagerWindowSizeClosedChat.width) + Consumer( + builder: (_, model, child) => SizedBox( + width: realChatPageWidth, + child: allowRemoteCMModification() + ? buildSidePage() + : buildRemoteBlock( + child: buildSidePage(), + block: _sidePageBlock, + mask: true), + )), + SizedBox( + width: realClosedWidth, + child: SizedBox( + width: realClosedWidth, + child: allowRemoteCMModification() + ? pageView + : buildRemoteBlock( + child: _buildKeyEventBlock(pageView), + block: _controlPageBlock, + mask: false, + ))), + ]); + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: row, + ); + }, + ), + ), + ); + } + + Widget buildSidePage() { + final selected = gFFI.serverModel.tabController.state.value.selected; + if (selected < 0 || selected >= gFFI.serverModel.clients.length) { + return Offstage(); + } + final clientType = gFFI.serverModel.clients[selected].type_(); + if (clientType == ClientType.file) { + return _FileTransferLogPage(); + } else { + return ChatPage(type: ChatPageType.desktopCM); + } + } + + Widget _buildKeyEventBlock(Widget child) { + return ExcludeFocus(child: child, excluding: true); + } + + Widget buildTitleBar() { + return SizedBox( + height: kDesktopRemoteTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const _AppIcon(), + Expanded( + child: GestureDetector( + onPanStart: (d) { + windowManager.startDragging(); + }, + child: Container( + color: Theme.of(context).colorScheme.background, + ), + ), + ), + const SizedBox( + width: 4.0, + ), + const _CloseButton() + ], + ), + ); + } + + Widget buildScrollJumper() { + final offstage = gFFI.serverModel.clients.length < 2; + final sc = gFFI.serverModel.tabController.state.value.scrollController; + return Offstage( + offstage: offstage, + child: Row( + children: [ + ActionIcon( + icon: Icons.arrow_left, iconSize: 22, onTap: sc.backward), + ActionIcon( + icon: Icons.arrow_right, iconSize: 22, onTap: sc.forward), + ], + )); + } + + Future handleWindowCloseButton() async { + var tabController = gFFI.serverModel.tabController; + final connLength = tabController.length; + if (connLength <= 1) { + windowManager.close(); + return true; + } else { + final bool res; + if (!option2bool(kOptionEnableConfirmClosingTabs, + bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + windowManager.close(); + } + return res; + } + } +} + +Widget buildConnectionCard(Client client) { + return Consumer( + builder: (context, value, child) => Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + key: ValueKey(client.id), + children: [ + _CmHeader(client: client), + client.type_() == ClientType.file || + client.type_() == ClientType.portForward || + client.type_() == ClientType.terminal || + client.disconnected + ? Offstage() + : _PrivilegeBoard(client: client), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: _CmControlPanel(client: client), + ), + ) + ], + ).paddingSymmetric(vertical: 4.0, horizontal: 8.0), + ); +} + +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: loadIcon(30), + ); + } +} + +class _CloseButton extends StatelessWidget { + const _CloseButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () { + windowManager.close(); + }, + icon: const Icon( + IconFont.close, + size: 18, + ), + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + ); + } +} + +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; + + final _time = 0.obs; + Timer? _timer; + + @override + void initState() { + super.initState(); + _timer = Timer.periodic(Duration(seconds: 1), (_) { + if (client.authorized && !client.disconnected) { + _time.value = _time.value + 1; + } + }); + // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. + WidgetsBinding.instance.addPostFrameCallback((_) { + gFFI.serverModel.tabController.onSelected?.call(client.id.toString()); + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + gradient: LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + Color(0xff00bfe1), + Color(0xff0071ff), + ], + ), + ), + margin: EdgeInsets.symmetric(horizontal: 5.0, vertical: 10.0), + padding: EdgeInsets.only( + top: 10.0, + bottom: 10.0, + left: 10.0, + right: 5.0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildClientAvatar().marginOnly(right: 10.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + child: Text( + client.name, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 20, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + )), + FittedBox( + child: Text( + "(${client.peerId})", + style: TextStyle(color: Colors.white, fontSize: 14), + ), + ), + if (client.type_() == ClientType.terminal) + FittedBox( + child: Text( + translate("Terminal"), + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + if (client.type_() == ClientType.file) + FittedBox( + child: Text( + translate("File Transfer"), + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + if (client.type_() == ClientType.camera) + FittedBox( + child: Text( + translate("View Camera"), + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + if (client.portForward.isNotEmpty) + FittedBox( + child: Text( + "Port Forward: ${client.portForward}", + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + SizedBox(height: 10.0), + FittedBox( + child: Row( + children: [ + Text( + client.authorized + ? client.disconnected + ? translate("Disconnected") + : translate("Connected") + : "${translate("Request access to your device")}...", + style: TextStyle(color: Colors.white), + ).marginOnly(right: 8.0), + if (client.authorized) + Obx( + () => Text( + formatDurationToTime( + Duration(seconds: _time.value), + ), + style: TextStyle(color: Colors.white), + ), + ) + ], + )) + ], + ), + ), + Offstage( + offstage: !client.authorized || + (client.type_() != ClientType.remote && + client.type_() != ClientType.file && + client.type_() != ClientType.camera), + child: IconButton( + onPressed: () => checkClickTime(client.id, () { + if (client.type_() == ClientType.file) { + gFFI.chatModel.toggleCMFilePage(); + } else { + gFFI.chatModel + .toggleCMChatPage(MessageKey(client.peerId, client.id)); + } + }), + icon: SvgPicture.asset(client.type_() == ClientType.file + ? 'assets/file_transfer.svg' + : 'assets/chat2.svg'), + splashRadius: kDesktopIconButtonSplashRadius, + ), + ) + ], + ), + ); + } + + @override + bool get wantKeepAlive => true; + + Widget _buildClientAvatar() { + return buildAvatarWidget( + avatar: client.avatar, + size: 70, + borderRadius: 15, + fallback: _buildInitialAvatar(), + ) ?? + _buildInitialAvatar(); + } + + Widget _buildInitialAvatar() { + return Container( + width: 70, + height: 70, + alignment: Alignment.center, + decoration: BoxDecoration( + color: str2color(client.name), + borderRadius: BorderRadius.circular(15.0), + ), + child: Text( + client.name.isNotEmpty ? client.name[0] : '?', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 55, + ), + ), + ); + } +} + +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, IconData iconData, + Function(bool)? onTap, String tooltipText) { + return Tooltip( + message: "$tooltipText: ${enabled ? "ON" : "OFF"}", + waitDuration: Duration.zero, + child: Container( + decoration: BoxDecoration( + color: enabled ? MyTheme.accent : Colors.grey[700], + borderRadius: BorderRadius.circular(10.0), + ), + padding: EdgeInsets.all(8.0), + child: InkWell( + onTap: () => + checkClickTime(widget.client.id, () => onTap?.call(!enabled)), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: Icon( + iconData, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final crossAxisCount = 4; + final spacing = 10.0; + return Container( + width: double.infinity, + height: 160.0, + margin: EdgeInsets.all(5.0), + padding: EdgeInsets.all(5.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + color: Theme.of(context).colorScheme.background, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 1, + offset: Offset(0, 1.5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + translate("Permissions"), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ).marginOnly(left: 4.0, bottom: 8.0), + Expanded( + child: GridView.count( + crossAxisCount: crossAxisCount, + padding: EdgeInsets.symmetric(horizontal: spacing), + mainAxisSpacing: spacing, + crossAxisSpacing: spacing, + children: client.type_() == ClientType.camera + ? [ + buildPermissionIcon( + client.audio, + Icons.volume_up_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "audio", + enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, + translate('Enable audio'), + ), + buildPermissionIcon( + client.recording, + Icons.videocam_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "recording", + enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, + translate('Enable recording session'), + ), + ] + : [ + buildPermissionIcon( + client.keyboard, + Icons.keyboard, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "keyboard", + enabled: enabled); + setState(() { + client.keyboard = enabled; + }); + }, + translate('Enable keyboard/mouse'), + ), + buildPermissionIcon( + client.clipboard, + Icons.assignment_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "clipboard", + enabled: enabled); + setState(() { + client.clipboard = enabled; + }); + }, + translate('Enable clipboard'), + ), + buildPermissionIcon( + client.audio, + Icons.volume_up_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "audio", + enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, + translate('Enable audio'), + ), + buildPermissionIcon( + client.file, + Icons.upload_file_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "file", + enabled: enabled); + setState(() { + client.file = enabled; + }); + }, + translate('Enable file copy and paste'), + ), + buildPermissionIcon( + client.restart, + Icons.restart_alt_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "restart", + enabled: enabled); + setState(() { + client.restart = enabled; + }); + }, + translate('Enable remote restart'), + ), + buildPermissionIcon( + client.recording, + Icons.videocam_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "recording", + enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, + translate('Enable recording session'), + ), + // only windows support block input + if (isWindows) + buildPermissionIcon( + client.blockInput, + Icons.block, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "block_input", + enabled: enabled); + setState(() { + client.blockInput = enabled; + }); + }, + translate('Enable blocking user input'), + ) + ], + ), + ), + ], + ), + ); + } +} + +const double buttonBottomMargin = 8; + +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 + ? client.disconnected + ? buildDisconnected(context) + : buildAuthorized(context) + : buildUnAuthorized(context); + } + + buildAuthorized(BuildContext context) { + final bool canElevate = bind.cmCanElevate(); + final model = Provider.of(context); + final showElevation = canElevate && + model.showElevation && + client.type_() == ClientType.remote; + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Offstage( + offstage: !client.inVoiceCall, + child: Row( + children: [ + Expanded( + child: buildButton(context, + color: MyTheme.accent, + onClick: null, onTapDown: (details) async { + final devicesInfo = + await AudioInput.getDevicesInfo(true, true); + List devices = devicesInfo['devices'] as List; + if (devices.isEmpty) { + msgBox( + gFFI.sessionId, + 'custom-nocancel-info', + 'Prompt', + 'no_audio_input_device_tip', + '', + gFFI.dialogManager, + ); + return; + } + + String currentDevice = devicesInfo['current'] as String; + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + final position = RelativeRect.fromLTRB(x, y, x, y); + showMenu( + context: context, + position: position, + items: devices + .map((d) => PopupMenuItem( + value: d, + height: 18, + padding: EdgeInsets.zero, + onTap: () => AudioInput.setDevice(d, true, true), + child: IgnorePointer( + child: RadioMenuButton( + value: d, + groupValue: currentDevice, + onChanged: (v) { + if (v != null) + AudioInput.setDevice(v, true, true); + }, + child: Container( + child: Text( + d, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + constraints: BoxConstraints( + maxWidth: + kConnectionManagerWindowSizeClosedChat + .width - + 80), + ), + )), + )) + .toList(), + ); + }, + icon: Icon( + Icons.call_rounded, + color: Colors.white, + size: 14, + ), + text: "Audio input", + textColor: Colors.white), + ), + Expanded( + child: buildButton( + context, + color: Colors.red, + onClick: () => closeVoiceCall(), + icon: Icon( + Icons.call_end_rounded, + color: Colors.white, + size: 14, + ), + text: "Stop voice call", + textColor: Colors.white, + ), + ) + ], + ), + ), + Offstage( + offstage: !client.incomingVoiceCall, + child: Row( + children: [ + Expanded( + child: buildButton(context, + color: MyTheme.accent, + onClick: () => handleVoiceCall(true), + icon: Icon( + Icons.call_rounded, + color: Colors.white, + size: 14, + ), + text: "Accept", + textColor: Colors.white), + ), + Expanded( + child: buildButton( + context, + color: Colors.red, + onClick: () => handleVoiceCall(false), + icon: Icon( + Icons.phone_disabled_rounded, + color: Colors.white, + size: 14, + ), + text: "Dismiss", + textColor: Colors.white, + ), + ) + ], + ), + ), + Offstage( + offstage: !client.fromSwitch, + child: buildButton(context, + color: Colors.purple, + onClick: () => handleSwitchBack(context), + icon: Icon(Icons.reply, color: Colors.white), + text: "Switch Sides", + textColor: Colors.white), + ), + Offstage( + offstage: !showElevation, + child: buildButton( + context, + color: MyTheme.accent, + onClick: () { + handleElevate(context); + windowManager.minimize(); + }, + icon: Icon( + Icons.security_rounded, + color: Colors.white, + size: 14, + ), + text: 'Elevate', + textColor: Colors.white, + ), + ), + Row( + children: [ + Expanded( + child: buildButton(context, + color: Colors.redAccent, + onClick: handleDisconnect, + text: 'Disconnect', + icon: Icon( + Icons.link_off_rounded, + color: Colors.white, + size: 14, + ), + textColor: Colors.white), + ), + ], + ) + ], + ).marginOnly(bottom: buttonBottomMargin); + } + + buildDisconnected(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: buildButton(context, + color: MyTheme.accent, + onClick: handleClose, + text: 'Close', + textColor: Colors.white)), + ], + ).marginOnly(bottom: buttonBottomMargin); + } + + buildUnAuthorized(BuildContext context) { + final bool canElevate = bind.cmCanElevate(); + final model = Provider.of(context); + final showElevation = canElevate && + model.showElevation && + client.type_() == ClientType.remote; + final showAccept = model.approveMode != 'password'; + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Offstage( + offstage: !showElevation || !showAccept, + child: buildButton(context, color: Colors.green[700], onClick: () { + handleAccept(context); + handleElevate(context); + windowManager.minimize(); + }, + text: 'Accept and Elevate', + icon: Icon( + Icons.security_rounded, + color: Colors.white, + size: 14, + ), + textColor: Colors.white, + tooltip: 'accept_and_elevate_btn_tooltip'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (showAccept) + Expanded( + child: Column( + children: [ + buildButton( + context, + color: MyTheme.accent, + onClick: () { + handleAccept(context); + windowManager.minimize(); + }, + text: 'Accept', + textColor: Colors.white, + ), + ], + ), + ), + Expanded( + child: buildButton( + context, + color: Colors.transparent, + border: Border.all(color: Colors.grey), + onClick: handleDisconnect, + text: 'Cancel', + textColor: null, + ), + ), + ], + ), + ], + ).marginOnly(bottom: buttonBottomMargin); + } + + Widget buildButton(BuildContext context, + {required Color? color, + GestureTapCallback? onClick, + Widget? icon, + BoxBorder? border, + required String text, + required Color? textColor, + String? tooltip, + GestureTapDownCallback? onTapDown}) { + assert(!(onClick == null && onTapDown == null)); + Widget textWidget; + if (icon != null) { + textWidget = Text( + translate(text), + style: TextStyle(color: textColor), + textAlign: TextAlign.center, + ); + } else { + textWidget = Expanded( + child: Text( + translate(text), + style: TextStyle(color: textColor), + textAlign: TextAlign.center, + ), + ); + } + final borderRadius = BorderRadius.circular(10.0); + final btn = Container( + height: 28, + decoration: BoxDecoration( + color: color, borderRadius: borderRadius, border: border), + child: InkWell( + borderRadius: borderRadius, + onTap: () { + if (onClick == null) return; + checkClickTime(client.id, onClick); + }, + onTapDown: (details) { + if (onTapDown == null) return; + checkClickTime(client.id, () { + onTapDown.call(details); + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Offstage(offstage: icon == null, child: icon).marginOnly(right: 5), + textWidget, + ], + ), + ), + ); + return (tooltip != null + ? Tooltip( + message: translate(tooltip), + child: btn, + ) + : btn) + .marginAll(4); + } + + void handleDisconnect() { + bind.cmCloseConnection(connId: client.id); + } + + void handleAccept(BuildContext context) { + final model = Provider.of(context, listen: false); + model.sendLoginResponse(client, true); + } + + void handleElevate(BuildContext context) { + final model = Provider.of(context, listen: false); + model.setShowElevation(false); + bind.cmElevatePortable(connId: client.id); + } + + void handleClose() async { + await bind.cmRemoveDisconnectedConnection(connId: client.id); + if (await bind.cmGetClientsLength() == 0) { + windowManager.close(); + } + } + + void handleSwitchBack(BuildContext context) { + bind.cmSwitchBack(connId: client.id); + } + + void handleVoiceCall(bool accept) { + bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept); + } + + void closeVoiceCall() { + bind.cmCloseVoiceCall(id: client.id); + } +} + +void checkClickTime(int id, Function() callback) async { + if (allowRemoteCMModification()) { + callback(); + return; + } + var clickCallbackTime = DateTime.now().millisecondsSinceEpoch; + await bind.cmCheckClickTime(connId: id); + Timer(const Duration(milliseconds: 120), () async { + var d = clickCallbackTime - await bind.cmGetClickTime(); + if (d > 120) callback(); + }); +} + +bool allowRemoteCMModification() { + return option2bool(kOptionAllowRemoteCmModification, + bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification)); +} + +class _FileTransferLogPage extends StatefulWidget { + _FileTransferLogPage({Key? key}) : super(key: key); + + @override + State<_FileTransferLogPage> createState() => __FileTransferLogPageState(); +} + +class __FileTransferLogPageState extends State<_FileTransferLogPage> { + @override + Widget build(BuildContext context) { + return statusList(); + } + + Widget generateCard(Widget child) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: child, + ); + } + + iconLabel(CmFileLog item) { + switch (item.action) { + case CmFileAction.none: + return Container(); + case CmFileAction.localToRemote: + case CmFileAction.remoteToLocal: + return Column( + children: [ + Transform.rotate( + angle: item.action == CmFileAction.remoteToLocal ? 0 : pi, + child: SvgPicture.asset( + "assets/arrow.svg", + colorFilter: svgColor(Theme.of(context).tabBarTheme.labelColor), + ), + ), + Text(item.action == CmFileAction.remoteToLocal + ? translate('Send') + : translate('Receive')) + ], + ); + case CmFileAction.remove: + return Column( + children: [ + Icon( + Icons.delete, + color: Theme.of(context).tabBarTheme.labelColor, + ), + Text(translate('Delete')) + ], + ); + case CmFileAction.createDir: + return Column( + children: [ + Icon( + Icons.create_new_folder, + color: Theme.of(context).tabBarTheme.labelColor, + ), + Text(translate('Create Folder')) + ], + ); + case CmFileAction.rename: + return Column( + children: [ + Icon( + Icons.drive_file_move_outlined, + color: Theme.of(context).tabBarTheme.labelColor, + ), + Text(translate('Rename')) + ], + ); + } + } + + Widget statusList() { + return PreferredSize( + preferredSize: const Size(200, double.infinity), + child: Container( + padding: const EdgeInsets.all(12.0), + child: Obx( + () { + final jobTable = gFFI.cmFileModel.currentJobTable; + statusListView(List jobs) => ListView.builder( + controller: ScrollController(), + itemBuilder: (BuildContext context, int index) { + final item = jobs[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: generateCard( + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 50, + child: iconLabel(item), + ).paddingOnly(left: 15), + const SizedBox( + width: 16.0, + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + item.fileName, + ).paddingSymmetric(vertical: 10), + if (item.totalSize > 0) + Text( + '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + ), + if (item.totalSize > 0) + Offstage( + offstage: item.state != + JobState.inProgress, + child: Text( + '${translate("Speed")} ${readableFileSize(item.speed)}/s', + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + ), + ), + Offstage( + offstage: !(item.isTransfer() && + item.state != + JobState.inProgress), + child: Text( + translate( + item.display(), + ), + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + ), + ), + if (item.totalSize > 0) + Offstage( + offstage: item.state != + JobState.inProgress, + child: LinearPercentIndicator( + padding: + EdgeInsets.only(right: 15), + animateFromLastPercent: true, + center: Text( + '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', + ), + barRadius: Radius.circular(15), + percent: item.finishedSize / + item.totalSize, + progressColor: MyTheme.accent, + backgroundColor: + Theme.of(context).hoverColor, + lineHeight: + kDesktopFileTransferRowHeight, + ).paddingSymmetric(vertical: 15), + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [], + ), + ], + ), + ], + ).paddingSymmetric(vertical: 10), + ), + ); + }, + itemCount: jobTable.length, + ); + + return jobTable.isEmpty + ? generateCard( + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + "assets/transfer.svg", + colorFilter: svgColor( + Theme.of(context).tabBarTheme.labelColor), + height: 40, + ).paddingOnly(bottom: 10), + Text( + translate("No transfers in progress"), + textAlign: TextAlign.center, + textScaler: TextScaler.linear(1.20), + style: TextStyle( + color: + Theme.of(context).tabBarTheme.labelColor), + ), + ], + ), + ), + ) + : statusListView(jobTable); + }, + )), + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_connection_manager.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_connection_manager.dart new file mode 100644 index 0000000..91b8baa --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_connection_manager.dart @@ -0,0 +1,98 @@ +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import '../../models/model.dart'; + +/// Manages terminal connections to ensure one FFI instance per peer +class TerminalConnectionManager { + static final Map _connections = {}; + static final Map _connectionRefCount = {}; + + // Track service IDs per peer + static final Map _serviceIds = {}; + + /// Get or create an FFI instance for a peer + static FFI getConnection({ + required String peerId, + required String? password, + required bool? isSharedPassword, + required bool? forceRelay, + required String? connToken, + }) { + final existingFfi = _connections[peerId]; + if (existingFfi != null && !existingFfi.closed) { + // Increment reference count + _connectionRefCount[peerId] = (_connectionRefCount[peerId] ?? 0) + 1; + debugPrint('[TerminalConnectionManager] Reusing existing connection for peer $peerId. Reference count: ${_connectionRefCount[peerId]}'); + return existingFfi; + } + + // Create new FFI instance for first terminal + debugPrint('[TerminalConnectionManager] Creating new terminal connection for peer $peerId'); + final ffi = FFI(null); + ffi.start( + peerId, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay, + connToken: connToken, + isTerminal: true, + ); + + _connections[peerId] = ffi; + _connectionRefCount[peerId] = 1; + + // Register the FFI instance with Get for dependency injection + Get.put(ffi, tag: 'terminal_$peerId'); + + debugPrint('[TerminalConnectionManager] New connection created. Total connections: ${_connections.length}'); + return ffi; + } + + /// Release a connection reference + static void releaseConnection(String peerId) { + final refCount = _connectionRefCount[peerId] ?? 0; + debugPrint('[TerminalConnectionManager] Releasing connection for peer $peerId. Current ref count: $refCount'); + + if (refCount <= 1) { + // Last reference, close the connection + final ffi = _connections[peerId]; + if (ffi != null) { + debugPrint('[TerminalConnectionManager] Closing connection for peer $peerId (last reference)'); + ffi.close(); + _connections.remove(peerId); + _connectionRefCount.remove(peerId); + Get.delete(tag: 'terminal_$peerId'); + } + } else { + // Decrement reference count + _connectionRefCount[peerId] = refCount - 1; + debugPrint('[TerminalConnectionManager] Connection still in use. New ref count: ${_connectionRefCount[peerId]}'); + } + } + + /// Check if a connection exists for a peer + static bool hasConnection(String peerId) { + final ffi = _connections[peerId]; + return ffi != null && !ffi.closed; + } + + /// Get existing connection without creating new one + static FFI? getExistingConnection(String peerId) { + return _connections[peerId]; + } + + /// Get connection count for debugging + static int getConnectionCount() => _connections.length; + + /// Get terminal count for a peer + static int getTerminalCount(String peerId) => _connectionRefCount[peerId] ?? 0; + + /// Get service ID for a peer + static String? getServiceId(String peerId) => _serviceIds[peerId]; + + /// Set service ID for a peer + static void setServiceId(String peerId, String serviceId) { + _serviceIds[peerId] = serviceId; + debugPrint('[TerminalConnectionManager] Service ID for $peerId: $serviceId'); + } +} \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_page.dart new file mode 100644 index 0000000..0070cd7 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_page.dart @@ -0,0 +1,205 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/terminal_model.dart'; +import 'package:xterm/xterm.dart'; +import 'terminal_connection_manager.dart'; + +class TerminalPage extends StatefulWidget { + TerminalPage({ + Key? key, + required this.id, + required this.password, + required this.tabController, + required this.isSharedPassword, + required this.terminalId, + required this.tabKey, + this.forceRelay, + this.connToken, + }) : super(key: key); + final String id; + final String? password; + final DesktopTabController tabController; + final bool? forceRelay; + final bool? isSharedPassword; + final String? connToken; + final int terminalId; + /// Tab key for focus management, passed from parent to avoid duplicate construction + final String tabKey; + final SimpleWrapper?> _lastState = SimpleWrapper(null); + + FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi; + + @override + State createState() { + final state = _TerminalPageState(); + _lastState.value = state; + return state; + } +} + +class _TerminalPageState extends State + with AutomaticKeepAliveClientMixin { + late FFI _ffi; + late TerminalModel _terminalModel; + double? _cellHeight; + final FocusNode _terminalFocusNode = FocusNode(canRequestFocus: false); + StreamSubscription? _tabStateSubscription; + + @override + void initState() { + super.initState(); + + // Listen for tab selection changes to request focus + _tabStateSubscription = widget.tabController.state.listen(_onTabStateChanged); + + // Use shared FFI instance from connection manager + _ffi = TerminalConnectionManager.getConnection( + peerId: widget.id, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + connToken: widget.connToken, + ); + + // Create terminal model with specific terminal ID + _terminalModel = TerminalModel(_ffi, widget.terminalId); + debugPrint( + '[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); + + _terminalModel.onResizeExternal = (w, h, pw, ph) { + _cellHeight = ph * 1.0; + + // Enable focus once terminal has valid dimensions (first valid resize) + if (!_terminalFocusNode.canRequestFocus && w > 0 && h > 0) { + _terminalFocusNode.canRequestFocus = true; + // Auto-focus if this tab is currently selected + _requestFocusIfSelected(); + } + + // Schedule the setState for the next frame + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() {}); + } + }); + }; + + // Register this terminal model with FFI for event routing + _ffi.registerTerminalModel(widget.terminalId, _terminalModel); + + // Initialize terminal connection + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.tabController.onSelected?.call(widget.id); + + // Check if this is a new connection or additional terminal + // Note: When a connection exists, the ref count will be > 1 after this terminal is added + final isExistingConnection = + TerminalConnectionManager.hasConnection(widget.id) && + TerminalConnectionManager.getTerminalCount(widget.id) > 1; + + if (!isExistingConnection) { + // First terminal - show loading dialog, wait for onReady + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + } else { + // Additional terminal - connection already established + // Open the terminal directly + _terminalModel.openTerminal(); + } + }); + } + + @override + void dispose() { + // Cancel tab state subscription to prevent memory leak + _tabStateSubscription?.cancel(); + // Unregister terminal model from FFI + _ffi.unregisterTerminalModel(widget.terminalId); + _terminalModel.dispose(); + _terminalFocusNode.dispose(); + // Release connection reference instead of closing directly + TerminalConnectionManager.releaseConnection(widget.id); + super.dispose(); + } + + void _onTabStateChanged(DesktopTabState state) { + // Check if this tab is now selected and request focus + if (state.selected >= 0 && state.selected < state.tabs.length) { + final selectedTab = state.tabs[state.selected]; + if (selectedTab.key == widget.tabKey && mounted) { + _requestFocusIfSelected(); + } + } + } + + void _requestFocusIfSelected() { + if (!mounted || !_terminalFocusNode.canRequestFocus) return; + // Use post-frame callback to ensure widget is fully laid out in focus tree + WidgetsBinding.instance.addPostFrameCallback((_) { + // Re-check conditions after frame: mounted, focusable, still selected, not already focused + if (!mounted || !_terminalFocusNode.canRequestFocus || _terminalFocusNode.hasFocus) return; + final state = widget.tabController.state.value; + if (state.selected >= 0 && state.selected < state.tabs.length) { + if (state.tabs[state.selected].key == widget.tabKey) { + _terminalFocusNode.requestFocus(); + } + } + }); + } + + // This method ensures that the number of visible rows is an integer by computing the + // extra space left after dividing the available height by the height of a single + // terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding. + EdgeInsets _calculatePadding(double heightPx) { + if (_cellHeight == null) { + return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0); + } + final rows = (heightPx / _cellHeight!).floor(); + final extraSpace = heightPx - rows * _cellHeight!; + final topBottom = extraSpace / 2.0; + return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: LayoutBuilder( + builder: (context, constraints) { + final heightPx = constraints.maxHeight; + return TerminalView( + _terminalModel.terminal, + controller: _terminalModel.terminalController, + focusNode: _terminalFocusNode, + // Note: autofocus is not used here because focus is managed manually + // via _onTabStateChanged() to handle tab switching properly. + backgroundOpacity: 0.7, + padding: _calculatePadding(heightPx), + onSecondaryTapDown: (details, offset) async { + final selection = _terminalModel.terminalController.selection; + if (selection != null) { + final text = _terminalModel.terminal.buffer.getText(selection); + _terminalModel.terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + _terminalModel.terminal.paste(text); + } + } + }, + ); + }, + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_tab_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_tab_page.dart new file mode 100644 index 0000000..28e59fb --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -0,0 +1,591 @@ +import 'dart:convert'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:get/get.dart'; + +import '../../models/platform_model.dart'; +import 'terminal_page.dart'; +import 'terminal_connection_manager.dart'; +import '../widgets/material_mod_popup_menu.dart' as mod_menu; +import '../widgets/popup_menu.dart'; +import 'package:bot_toast/bot_toast.dart'; + +class TerminalTabPage extends StatefulWidget { + final Map params; + + const TerminalTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _TerminalTabPageState(params); +} + +class _TerminalTabPageState extends State { + DesktopTabController get tabController => Get.find(); + + static const IconData selectedIcon = Icons.terminal; + static const IconData unselectedIcon = Icons.terminal_outlined; + int _nextTerminalId = 1; + // Lightweight idempotency guard for async close operations + final Set _closingTabs = {}; + // When true, all session cleanup should persist (window-level close in progress) + bool _windowClosing = false; + + _TerminalTabPageState(Map params) { + Get.put(DesktopTabController(tabType: DesktopTabType.terminal)); + tabController.onSelected = (id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; + tabController.onRemoved = (_, id) => onRemoveId(id); + final terminalId = params['terminalId'] ?? _nextTerminalId++; + tabController.add(_createTerminalTab( + peerId: params['id'], + terminalId: terminalId, + password: params['password'], + isSharedPassword: params['isSharedPassword'], + forceRelay: params['forceRelay'], + connToken: params['connToken'], + )); + } + + TabInfo _createTerminalTab({ + required String peerId, + required int terminalId, + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) { + final tabKey = '${peerId}_$terminalId'; + final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias'); + final tabLabel = + alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId'; + return TabInfo( + key: tabKey, + label: tabLabel, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => _closeTab(tabKey), + page: TerminalPage( + key: ValueKey(tabKey), + id: peerId, + terminalId: terminalId, + tabKey: tabKey, + password: password, + isSharedPassword: isSharedPassword, + tabController: tabController, + forceRelay: forceRelay, + connToken: connToken, + ), + ); + } + + /// Unified tab close handler for all close paths (button, shortcut, programmatic). + /// Shows audit dialog, cleans up session if not persistent, then removes the UI tab. + Future _closeTab(String tabKey) async { + // Idempotency guard: skip if already closing this tab + if (_closingTabs.contains(tabKey)) return; + _closingTabs.add(tabKey); + + try { + // Snapshot peerTabCount BEFORE any await to avoid race with concurrent + // _closeAllTabs clearing tabController (which would make the live count + // drop to 0 and incorrectly trigger session persistence). + // Note: the snapshot may become stale if other individual tabs are closed + // during the audit dialog, but this is an acceptable trade-off. + int? snapshotPeerTabCount; + final parsed = _parseTabKey(tabKey); + if (parsed != null) { + final (peerId, _) = parsed; + snapshotPeerTabCount = tabController.state.value.tabs.where((t) { + final p = _parseTabKey(t.key); + return p != null && p.$1 == peerId; + }).length; + } + + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: tabKey, + tabController: tabController, + )) { + return; + } + + // Close terminal session if not in persistent mode. + // Wrapped separately so session cleanup failure never blocks UI tab removal. + try { + await _closeTerminalSessionIfNeeded(tabKey, + peerTabCount: snapshotPeerTabCount); + } catch (e) { + debugPrint('[TerminalTabPage] Session cleanup failed for $tabKey: $e'); + } + // Always close the tab from UI, regardless of session cleanup result + tabController.closeBy(tabKey); + } catch (e) { + debugPrint('[TerminalTabPage] Error closing tab $tabKey: $e'); + } finally { + _closingTabs.remove(tabKey); + } + } + + /// Close all tabs with session cleanup. + /// Used for window-level close operations (onDestroy, handleWindowCloseButton). + /// UI tabs are removed immediately; session cleanup runs in parallel with a + /// bounded timeout so window close is not blocked indefinitely. + Future _closeAllTabs() async { + _windowClosing = true; + final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList(); + // Remove all UI tabs immediately (same instant behavior as the old tabController.clear()) + tabController.clear(); + // Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout). + // Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls. + final futures = tabKeys + .where((tabKey) => !_closingTabs.contains(tabKey)) + .map((tabKey) async { + try { + await _closeTerminalSessionIfNeeded(tabKey, persistAll: true); + } catch (e) { + debugPrint('[TerminalTabPage] Session cleanup failed for $tabKey: $e'); + } + }).toList(); + if (futures.isNotEmpty) { + await Future.wait(futures).timeout( + const Duration(seconds: 4), + onTimeout: () { + debugPrint( + '[TerminalTabPage] Session cleanup timed out for batch close'); + return []; + }, + ); + } + } + + /// Close the terminal session on server side based on persistent mode. + /// + /// [persistAll] controls behavior when persistent mode is enabled: + /// - `true` (window close): persist all sessions, don't close any. + /// - `false` (tab close): only persist the last session for the peer, + /// close others so only the most recent disconnected session survives. + /// + /// Note: if [_windowClosing] is true, persistAll is forced to true so that + /// in-flight _closeTab() calls don't accidentally close sessions that the + /// window-close flow intends to preserve. + Future _closeTerminalSessionIfNeeded(String tabKey, + {bool persistAll = false, int? peerTabCount}) async { + // If window close is in progress, override to persist all sessions + // even if this call originated from an individual tab close. + if (_windowClosing) { + persistAll = true; + } + final parsed = _parseTabKey(tabKey); + if (parsed == null) return; + final (peerId, terminalId) = parsed; + + final ffi = TerminalConnectionManager.getExistingConnection(peerId); + if (ffi == null) return; + + final isPersistent = bind.sessionGetToggleOptionSync( + sessionId: ffi.sessionId, + arg: kOptionTerminalPersistent, + ); + + if (isPersistent) { + if (persistAll) { + // Window close: persist all sessions + return; + } + // Tab close: only persist if this is the last tab for this peer. + // Use the snapshot value if provided (avoids race with concurrent tab removal). + final effectivePeerTabCount = peerTabCount ?? + tabController.state.value.tabs.where((t) { + final p = _parseTabKey(t.key); + return p != null && p.$1 == peerId; + }).length; + if (effectivePeerTabCount <= 1) { + // Last tab for this peer — persist the session + return; + } + // Not the last tab — fall through to close the session + } + + final terminalModel = ffi.terminalModels[terminalId]; + if (terminalModel != null) { + // closeTerminal() has internal 3s timeout, no need for external timeout + await terminalModel.closeTerminal(); + } + } + + /// Parse tabKey (format: "peerId_terminalId") into its components. + /// Note: peerId may contain underscores, so we use lastIndexOf('_'). + /// Returns null if tabKey format is invalid. + (String peerId, int terminalId)? _parseTabKey(String tabKey) { + final lastUnderscore = tabKey.lastIndexOf('_'); + if (lastUnderscore <= 0) { + debugPrint('[TerminalTabPage] Invalid tabKey format: $tabKey'); + return null; + } + final terminalIdStr = tabKey.substring(lastUnderscore + 1); + final terminalId = int.tryParse(terminalIdStr); + if (terminalId == null) { + debugPrint('[TerminalTabPage] Invalid terminalId in tabKey: $tabKey'); + return null; + } + final peerId = tabKey.substring(0, lastUnderscore); + return (peerId, terminalId); + } + + Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) { + final List> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + + // New tab menu item + menu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('New tab'), + style: style, + ), + proc: () { + _addNewTerminal(peerId); + cancelFunc(); + // Also try to close any BotToast overlays + BotToast.cleanAll(); + }, + padding: padding, + )); + + menu.add(MenuEntryDivider()); + + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate('Keep terminal sessions on disconnect'), + getter: () async { + final ffi = Get.find(tag: 'terminal_$peerId'); + return bind.sessionGetToggleOptionSync( + sessionId: ffi.sessionId, + arg: kOptionTerminalPersistent, + ); + }, + setter: (bool v) async { + final ffi = Get.find(tag: 'terminal_$peerId'); + await bind.sessionToggleOption( + sessionId: ffi.sessionId, + value: kOptionTerminalPersistent, + ); + }, + padding: padding, + )); + + return mod_menu.PopupMenu( + items: menu + .map((e) => e.build( + context, + const MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight, + ), + )) + .expand((i) => i) + .toList(), + ); + } + + @override + void initState() { + super.initState(); + + // Add keyboard shortcut handler + HardwareKeyboard.instance.addHandler(_handleKeyEvent); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "[Remote Terminal] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + if (call.method == kWindowEventNewTerminal) { + final args = jsonDecode(call.arguments); + final id = args['id']; + windowOnTop(windowId()); + // Allow multiple terminals for the same connection + final terminalId = args['terminalId'] ?? _nextTerminalId++; + tabController.add(_createTerminalTab( + peerId: id, + terminalId: terminalId, + password: args['password'], + isSharedPassword: args['isSharedPassword'], + forceRelay: args['forceRelay'], + connToken: args['connToken'], + )); + } else if (call.method == kWindowEventRestoreTerminalSessions) { + _restoreSessions(call.arguments); + } else if (call.method == "onDestroy") { + // Clean up sessions before window destruction (bounded wait) + await _closeAllTabs(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } else if (call.method == kWindowEventActiveSession) { + if (tabController.state.value.tabs.isEmpty) { + return false; + } + final currentTab = tabController.state.value.selectedTabInfo; + assert(call.arguments is String, + "Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}"); + // Use lastIndexOf to handle peerIds containing underscores + final lastUnderscore = currentTab.key.lastIndexOf('_'); + if (lastUnderscore > 0 && + currentTab.key.substring(0, lastUnderscore) == call.arguments) { + windowOnTop(windowId()); + return true; + } + return false; + } + }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.Terminal, windowId: windowId()); + }); + } + + @override + void dispose() { + HardwareKeyboard.instance.removeHandler(_handleKeyEvent); + super.dispose(); + } + + Future _restoreSessions(String arguments) async { + Map? args; + try { + args = jsonDecode(arguments) as Map; + } catch (e) { + debugPrint("Error parsing JSON arguments in _restoreSessions: $e"); + return; + } + final persistentSessions = + args['persistent_sessions'] as List? ?? []; + final sortedSessions = persistentSessions.whereType().toList()..sort(); + for (final terminalId in sortedSessions) { + _addNewTerminalForCurrentPeer(terminalId: terminalId); + // A delay is required to ensure the UI has sufficient time to update + // before adding the next terminal. Without this delay, `_TerminalPageState::dispose()` + // may be called prematurely while the tab widget is still in the tab controller. + // This behavior is likely due to a race condition between the UI rendering lifecycle + // and the addition of new tabs. Attempts to use `_TerminalPageState::addPostFrameCallback()` + // to wait for the previous page to be ready were unsuccessful, as the observed call sequence is: + // `initState() 2 -> dispose() 2 -> postFrameCallback() 2`, followed by `initState() 3`. + // The `Future.delayed` approach mitigates this issue by introducing a buffer period, + // allowing the UI to stabilize before proceeding. + await Future.delayed(const Duration(milliseconds: 300)); + } + } + + bool _handleKeyEvent(KeyEvent event) { + if (event is KeyDownEvent) { + // Use Cmd+T on macOS, Ctrl+Shift+T on other platforms + if (event.logicalKey == LogicalKeyboardKey.keyT) { + if (isMacOS && + HardwareKeyboard.instance.isMetaPressed && + !HardwareKeyboard.instance.isShiftPressed) { + // macOS: Cmd+T (standard for new tab) + _addNewTerminalForCurrentPeer(); + return true; + } else if (!isMacOS && + HardwareKeyboard.instance.isControlPressed && + HardwareKeyboard.instance.isShiftPressed) { + // Other platforms: Ctrl+Shift+T (to avoid conflict with Ctrl+T in terminal) + _addNewTerminalForCurrentPeer(); + return true; + } + } + + // Use Cmd+W on macOS, Ctrl+Shift+W on other platforms + if (event.logicalKey == LogicalKeyboardKey.keyW) { + if (isMacOS && + HardwareKeyboard.instance.isMetaPressed && + !HardwareKeyboard.instance.isShiftPressed) { + // macOS: Cmd+W (standard for close tab) + final currentTab = tabController.state.value.selectedTabInfo; + if (tabController.state.value.tabs.length > 1) { + _closeTab(currentTab.key); + return true; + } + } else if (!isMacOS && + HardwareKeyboard.instance.isControlPressed && + HardwareKeyboard.instance.isShiftPressed) { + // Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete) + final currentTab = tabController.state.value.selectedTabInfo; + if (tabController.state.value.tabs.length > 1) { + _closeTab(currentTab.key); + return true; + } + } + } + + // Use Alt+Left/Right for tab navigation (avoids conflicts) + if (HardwareKeyboard.instance.isAltPressed) { + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + // Previous tab + final currentIndex = tabController.state.value.selected; + if (currentIndex > 0) { + tabController.jumpTo(currentIndex - 1); + } + return true; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + // Next tab + final currentIndex = tabController.state.value.selected; + if (currentIndex < tabController.length - 1) { + tabController.jumpTo(currentIndex + 1); + } + return true; + } + } + + // Check for Cmd/Ctrl + Number (switch to specific tab) + final numberKeys = [ + LogicalKeyboardKey.digit1, + LogicalKeyboardKey.digit2, + LogicalKeyboardKey.digit3, + LogicalKeyboardKey.digit4, + LogicalKeyboardKey.digit5, + LogicalKeyboardKey.digit6, + LogicalKeyboardKey.digit7, + LogicalKeyboardKey.digit8, + LogicalKeyboardKey.digit9, + ]; + + for (int i = 0; i < numberKeys.length; i++) { + if (event.logicalKey == numberKeys[i] && + ((isMacOS && HardwareKeyboard.instance.isMetaPressed) || + (!isMacOS && HardwareKeyboard.instance.isControlPressed))) { + if (i < tabController.length) { + tabController.jumpTo(i); + return true; + } + } + } + } + return false; + } + + void _addNewTerminal(String peerId, {int? terminalId}) { + // Find first tab for this peer to get connection parameters + final firstTab = tabController.state.value.tabs.firstWhere( + (tab) { + final last = tab.key.lastIndexOf('_'); + return last > 0 && tab.key.substring(0, last) == peerId; + }, + ); + if (firstTab.page is TerminalPage) { + final page = firstTab.page as TerminalPage; + final newTerminalId = terminalId ?? _nextTerminalId++; + if (terminalId != null && terminalId >= _nextTerminalId) { + _nextTerminalId = terminalId + 1; + } + tabController.add(_createTerminalTab( + peerId: peerId, + terminalId: newTerminalId, + password: page.password, + isSharedPassword: page.isSharedPassword, + forceRelay: page.forceRelay, + connToken: page.connToken, + )); + } + } + + void _addNewTerminalForCurrentPeer({int? terminalId}) { + final currentTab = tabController.state.value.selectedTabInfo; + final parsed = _parseTabKey(currentTab.key); + if (parsed == null) return; + final (peerId, _) = parsed; + _addNewTerminal(peerId, terminalId: terminalId); + } + + @override + Widget build(BuildContext context) { + final child = Scaffold( + backgroundColor: Theme.of(context).cardColor, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: _buildAddButton(), + selectedBorderColor: MyTheme.accent, + labelGetter: DesktopTab.tablabelGetter, + tabMenuBuilder: (key) { + final parsed = _parseTabKey(key); + if (parsed == null) return Container(); + final (peerId, _) = parsed; + return _tabMenuBuilder(peerId, () {}); + }, + )); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + )); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : SubWindowDragToResizeArea( + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, + windowId: stateGlobal.windowId, + ); + } + + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } + + Widget _buildAddButton() { + return ActionIcon( + message: 'New tab', + icon: IconFont.add, + onTap: () { + _addNewTerminalForCurrentPeer(); + }, + isClose: false, + ); + } + + Future handleWindowCloseButton() async { + final connLength = tabController.state.value.tabs.length; + if (connLength == 1) { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: tabController.state.value.tabs[0].key, + tabController: tabController, + )) { + return false; + } + } + if (connLength <= 1) { + await _closeAllTabs(); + return true; + } else { + final bool res; + if (!option2bool(kOptionEnableConfirmClosingTabs, + bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + await _closeAllTabs(); + } + return res; + } + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/view_camera_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/view_camera_page.dart new file mode 100644 index 0000000..c45ec4d --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/view_camera_page.dart @@ -0,0 +1,717 @@ +import 'dart:async'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/remote_input.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_hbb/models/state_model.dart'; + +import '../../consts.dart'; +import '../../common/widgets/overlay.dart'; +import '../../common.dart'; +import '../../common/widgets/dialog.dart'; +import '../../common/widgets/toolbar.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../../common/shared_state.dart'; +import '../../utils/image.dart'; +import '../widgets/remote_toolbar.dart'; +import '../widgets/kb_layout_type_chooser.dart'; +import '../widgets/tabbar_widget.dart'; + +import 'package:flutter_hbb/native/custom_cursor.dart' + if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart'; + +final SimpleWrapper _firstEnterImage = SimpleWrapper(false); + +// Used to skip session close if "move to new window" is clicked. +final Map closeSessionOnDispose = {}; + +class ViewCameraPage extends StatefulWidget { + ViewCameraPage({ + Key? key, + required this.id, + required this.toolbarState, + this.sessionId, + this.tabWindowId, + this.password, + this.display, + this.displays, + this.tabController, + this.connToken, + this.forceRelay, + this.isSharedPassword, + }) : super(key: key) { + initSharedStates(id); + } + + final String id; + final SessionID? sessionId; + final int? tabWindowId; + final int? display; + final List? displays; + final String? password; + final ToolbarState toolbarState; + final bool? forceRelay; + final bool? isSharedPassword; + final String? connToken; + final SimpleWrapper?> _lastState = SimpleWrapper(null); + final DesktopTabController? tabController; + + FFI get ffi => (_lastState.value! as _ViewCameraPageState)._ffi; + + @override + State createState() { + final state = _ViewCameraPageState(id); + _lastState.value = state; + return state; + } +} + +class _ViewCameraPageState extends State + with AutomaticKeepAliveClientMixin, MultiWindowListener { + Timer? _timer; + String keyboardMode = "legacy"; + bool _isWindowBlur = false; + final _cursorOverImage = false.obs; + final _uniqueKey = UniqueKey(); + + var _blockableOverlayState = BlockableOverlayState(); + + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); + + // We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar` + // to identify the toolbar instance and its callback function. + int? _instanceIdOnEnterOrLeaveImage4Toolbar; + Function(bool)? _onEnterOrLeaveImage4Toolbar; + + late FFI _ffi; + + SessionID get sessionId => _ffi.sessionId; + + _ViewCameraPageState(String id) { + _initStates(id); + } + + void _initStates(String id) {} + + @override + void initState() { + super.initState(); + _ffi = FFI(widget.sessionId); + Get.put(_ffi, tag: widget.id); + _ffi.imageModel.addCallbackOnFirstImage((String peerId) { + showKBLayoutTypeChooserIfNeeded( + _ffi.ffiModel.pi.platform, _ffi.dialogManager); + _ffi.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); + }); + _ffi.start( + widget.id, + isViewCamera: true, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + tabWindowId: widget.tabWindowId, + display: widget.display, + displays: widget.displays, + connToken: widget.connToken, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + WakelockManager.enable(_uniqueKey); + + _ffi.ffiModel.updateEventListener(sessionId, widget.id); + if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); + _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); + _ffi.dialogManager.loadMobileActionsOverlayVisible(); + DesktopMultiWindow.addListener(this); + // if (!_isCustomCursorInited) { + // customCursorController.registerNeedUpdateCursorCallback( + // (String? lastKey, String? currentKey) async { + // if (_firstEnterImage.value) { + // _firstEnterImage.value = false; + // return true; + // } + // return lastKey == null || lastKey != currentKey; + // }); + // _isCustomCursorInited = true; + // } + + _blockableOverlayState.applyFfi(_ffi); + // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.tabController?.onSelected?.call(widget.id); + }); + } + + @override + void onWindowBlur() { + super.onWindowBlur(); + // On windows, we use `focus` way to handle keyboard better. + // Now on Linux, there's some rdev issues which will break the input. + // We disable the `focus` way for non-Windows temporarily. + if (isWindows) { + _isWindowBlur = true; + // unfocus the primary-focus when the whole window is lost focus, + // and let OS to handle events instead. + _rawKeyFocusNode.unfocus(); + } + stateGlobal.isFocused.value = false; + } + + @override + void onWindowFocus() { + super.onWindowFocus(); + // See [onWindowBlur]. + if (isWindows) { + _isWindowBlur = false; + } + stateGlobal.isFocused.value = true; + } + + @override + void onWindowRestore() { + super.onWindowRestore(); + // On windows, we use `onWindowRestore` way to handle window restore from + // a minimized state. + if (isWindows) { + _isWindowBlur = false; + } + WakelockManager.enable(_uniqueKey); + } + + // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not. + @override + void onWindowMaximize() { + super.onWindowMaximize(); + WakelockManager.enable(_uniqueKey); + } + + @override + void onWindowMinimize() { + super.onWindowMinimize(); + WakelockManager.disable(_uniqueKey); + } + + @override + void onWindowEnterFullScreen() { + super.onWindowEnterFullScreen(); + if (isMacOS) { + stateGlobal.setFullscreen(true); + } + } + + @override + void onWindowLeaveFullScreen() { + super.onWindowLeaveFullScreen(); + if (isMacOS) { + stateGlobal.setFullscreen(false); + } + } + + @override + Future dispose() async { + final closeSession = closeSessionOnDispose.remove(widget.id) ?? true; + + // https://github.com/flutter/flutter/issues/64935 + super.dispose(); + debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}"); + _ffi.textureModel.onViewCameraPageDispose(closeSession); + if (closeSession) { + // ensure we leave this session, this is a double check + _ffi.inputModel.enterOrLeave(false); + } + DesktopMultiWindow.removeListener(this); + _ffi.dialogManager.hideMobileActionsOverlay(); + _ffi.imageModel.disposeImage(); + _ffi.cursorModel.disposeImages(); + _rawKeyFocusNode.dispose(); + await _ffi.close(closeSession: closeSession); + _timer?.cancel(); + _ffi.dialogManager.dismissAll(); + if (closeSession) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + } + WakelockManager.disable(_uniqueKey); + await Get.delete(tag: widget.id); + removeSharedStates(widget.id); + } + + Widget emptyOverlay() => BlockableOverlay( + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + state: _blockableOverlayState, + underlying: Container( + color: Colors.transparent, + ), + ); + + Widget buildBody(BuildContext context) { + remoteToolbar(BuildContext context) => RemoteToolbar( + id: widget.id, + ffi: _ffi, + state: widget.toolbarState, + onEnterOrLeaveImageSetter: (id, func) { + _instanceIdOnEnterOrLeaveImage4Toolbar = id; + _onEnterOrLeaveImage4Toolbar = func; + }, + onEnterOrLeaveImageCleaner: (id) { + // If _instanceIdOnEnterOrLeaveImage4Toolbar != id + // it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar. + if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) { + _instanceIdOnEnterOrLeaveImage4Toolbar = null; + _onEnterOrLeaveImage4Toolbar = null; + } + }, + setRemoteState: setState, + ); + + bodyWidget() { + return Stack( + children: [ + Container( + color: kColorCanvas, + child: getBodyForDesktop(context), + ), + Stack( + children: [ + _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isTrue + ? emptyOverlay() + : () { + if (!_ffi.ffiModel.isPeerAndroid) { + return Offstage(); + } else { + return Obx(() => Offstage( + offstage: _ffi.dialogManager + .mobileActionsOverlayVisible.isFalse, + child: Overlay(initialEntries: [ + makeMobileActionsOverlayEntry( + () => _ffi.dialogManager + .setMobileActionsOverlayVisible(false), + ffi: _ffi, + ) + ]), + )); + } + }(), + // Use Overlay to enable rebuild every time on menu button click. + _ffi.ffiModel.pi.isSet.isTrue + ? Overlay( + initialEntries: [OverlayEntry(builder: remoteToolbar)]) + : remoteToolbar(context), + _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(), + ], + ), + ], + ); + } + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: Obx(() { + final imageReady = _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isFalse; + if (imageReady) { + // If the privacy mode(disable physical displays) is switched, + // we should not dismiss the dialog immediately. + if (DateTime.now().difference(togglePrivacyModeTime) > + const Duration(milliseconds: 3000)) { + // `dismissAll()` is to ensure that the state is clean. + // It's ok to call dismissAll() here. + _ffi.dialogManager.dismissAll(); + // Recreate the block state to refresh the state. + _blockableOverlayState = BlockableOverlayState(); + _blockableOverlayState.applyFfi(_ffi); + } + // Block the whole `bodyWidget()` when dialog shows. + return BlockableOverlay( + underlying: bodyWidget(), + state: _blockableOverlayState, + ); + } else { + // `_blockableOverlayState` is not recreated here. + // The toolbar's block state won't work properly when reconnecting, but that's okay. + return bodyWidget(); + } + }), + ); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return WillPopScope( + onWillPop: () async { + clientClose(sessionId, _ffi); + 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), + ChangeNotifierProvider.value(value: _ffi.recordingModel), + ], child: buildBody(context))); + } + + void enterView(PointerEnterEvent evt) { + _cursorOverImage.value = true; + _firstEnterImage.value = true; + if (_onEnterOrLeaveImage4Toolbar != null) { + try { + _onEnterOrLeaveImage4Toolbar!(true); + } catch (e) { + // + } + } + // See [onWindowBlur]. + if (!isWindows) { + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + _ffi.inputModel.enterOrLeave(true); + } + } + + void leaveView(PointerExitEvent evt) { + if (_ffi.ffiModel.keyboard) { + _ffi.inputModel.tryMoveEdgeOnExit(evt.position); + } + + _cursorOverImage.value = false; + _firstEnterImage.value = false; + if (_onEnterOrLeaveImage4Toolbar != null) { + try { + _onEnterOrLeaveImage4Toolbar!(false); + } catch (e) { + // + } + } + // See [onWindowBlur]. + if (!isWindows) { + _ffi.inputModel.enterOrLeave(false); + } + } + + Widget _buildRawTouchAndPointerRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return RawTouchGestureDetectorRegion( + child: _buildRawPointerMouseRegion(child, onEnter, onExit), + ffi: _ffi, + isCamera: true, + ); + } + + Widget _buildRawPointerMouseRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return CameraRawPointerMouseRegion( + onEnter: onEnter, + onExit: onExit, + onPointerDown: (event) { + // A double check for blur status. + // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. + // Sometimes the system does not send the necessary focus event to flutter. We should manually + // handle this inconsistent status by setting `_isWindowBlur` to false. So we can + // ensure the grab-key thread is running when our users are clicking the remote canvas. + if (_isWindowBlur) { + debugPrint( + "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); + _isWindowBlur = false; + } + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + }, + inputModel: _ffi.inputModel, + child: child, + ); + } + + Widget getBodyForDesktop(BuildContext context) { + var paints = [ + MouseRegion(onEnter: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false); + }, onExit: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true); + }, child: LayoutBuilder(builder: (context, constraints) { + final c = Provider.of(context, listen: false); + Future.delayed(Duration.zero, () => c.updateViewStyle()); + final peerDisplay = CurrentDisplayState.find(widget.id); + return Obx( + () => _ffi.ffiModel.pi.isSet.isFalse + ? Container(color: Colors.transparent) + : Obx(() { + _ffi.textureModel.updateCurrentDisplay(peerDisplay.value); + return ImagePaint( + id: widget.id, + cursorOverImage: _cursorOverImage, + listenerBuilder: (child) => _buildRawTouchAndPointerRegion( + child, enterView, leaveView), + ffi: _ffi, + ); + }), + ); + })) + ]; + + paints.add( + Positioned( + top: 10, + right: 10, + child: _buildRawTouchAndPointerRegion( + QualityMonitor(_ffi.qualityMonitorModel), null, null), + ), + ); + return Stack( + children: paints, + ); + } + + @override + bool get wantKeepAlive => true; +} + +class ImagePaint extends StatefulWidget { + final FFI ffi; + final String id; + final RxBool cursorOverImage; + final Widget Function(Widget)? listenerBuilder; + + ImagePaint( + {Key? key, + required this.ffi, + required this.id, + required this.cursorOverImage, + this.listenerBuilder}) + : super(key: key); + + @override + State createState() => _ImagePaintState(); +} + +class _ImagePaintState extends State { + String get id => widget.id; + RxBool get cursorOverImage => widget.cursorOverImage; + Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; + + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + var c = Provider.of(context); + final s = c.scale; + + bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal; + + if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) { + final paintWidth = c.getDisplayWidth() * s; + final paintHeight = c.getDisplayHeight() * s; + final paintSize = Size(paintWidth, paintHeight); + final paintWidget = + m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender + ? _BuildPaintTextureRender( + c, s, Offset.zero, paintSize, isViewOriginal()) + : _buildScrollbarNonTextureRender(m, paintSize, s); + return NotificationListener( + onNotification: (notification) { + c.updateScrollPercent(); + return false; + }, + child: Container( + child: _buildCrossScrollbarFromLayout( + context, + _buildListener(paintWidget), + c.size, + paintSize, + c.scrollHorizontal, + c.scrollVertical, + )), + ); + } else { + if (c.size.width > 0 && c.size.height > 0) { + final paintWidget = + m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender + ? _BuildPaintTextureRender( + c, + s, + Offset( + isLinux ? c.x.toInt().toDouble() : c.x, + isLinux ? c.y.toInt().toDouble() : c.y, + ), + c.size, + isViewOriginal()) + : _buildScrollAutoNonTextureRender(m, c, s); + return Container(child: _buildListener(paintWidget)); + } else { + return Container(); + } + } + } + + Widget _buildScrollbarNonTextureRender( + ImageModel m, Size imageSize, double s) { + return CustomPaint( + size: imageSize, + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + ); + } + + Widget _buildScrollAutoNonTextureRender( + ImageModel m, CanvasModel c, double s) { + return CustomPaint( + size: Size(c.size.width, c.size.height), + painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + ); + } + + Widget _BuildPaintTextureRender( + CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) { + final ffiModel = c.parent.target!.ffiModel; + final displays = ffiModel.pi.getCurDisplays(); + final children = []; + final rect = ffiModel.rect; + if (rect == null) { + return Container(); + } + final curDisplay = ffiModel.pi.currentDisplay; + for (var i = 0; i < displays.length; i++) { + final textureId = widget.ffi.textureModel + .getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay); + if (true) { + // both "textureId.value != -1" and "true" seems ok + children.add(Positioned( + left: (displays[i].x - rect.left) * s + offset.dx, + top: (displays[i].y - rect.top) * s + offset.dy, + width: displays[i].width * s, + height: displays[i].height * s, + child: Obx(() => Texture( + textureId: textureId.value, + filterQuality: + isViewOriginal ? FilterQuality.none : FilterQuality.low, + )), + )); + } + } + return SizedBox( + width: size.width, + height: size.height, + child: Stack(children: children), + ); + } + + MouseCursor _buildCustomCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = cursor.cache ?? preDefaultCursor.cache; + return buildCursorOfCache(cursor, scale, cache); + } + + MouseCursor _buildDisabledCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = preForbiddenCursor.cache; + return buildCursorOfCache(cursor, scale, cache); + } + + Widget _buildCrossScrollbarFromLayout( + BuildContext context, + Widget child, + Size layoutSize, + Size size, + ScrollController horizontal, + ScrollController vertical, + ) { + var widget = child; + if (layoutSize.width < size.width) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: horizontal, + scrollDirection: Axis.horizontal, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Row( + children: [ + Container( + width: ((layoutSize.width - size.width) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.height < size.height) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: vertical, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Column( + children: [ + Container( + height: ((layoutSize.height - size.height) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.width < size.width) { + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: horizontal, + thumbVisibility: false, + trackVisibility: false, + notificationPredicate: layoutSize.height < size.height + ? (notification) => notification.depth == 1 + : defaultScrollNotificationPredicate, + child: widget, + ); + } + if (layoutSize.height < size.height) { + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: vertical, + thumbVisibility: false, + trackVisibility: false, + child: widget, + ); + } + + return Container( + child: widget, + width: layoutSize.width, + height: layoutSize.height, + ); + } + + Widget _buildListener(Widget child) { + if (listenerBuilder != null) { + return listenerBuilder!(child); + } else { + return child; + } + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/view_camera_tab_page.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/view_camera_tab_page.dart new file mode 100644 index 0000000..36fa623 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/pages/view_camera_tab_page.dart @@ -0,0 +1,522 @@ +import 'dart:convert'; +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/input_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_page.dart'; +import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:bot_toast/bot_toast.dart'; + +import '../../models/platform_model.dart'; + +class _MenuTheme { + static const Color blueColor = MyTheme.button; + // kMinInteractiveDimension + static const double height = 20.0; + static const double dividerHeight = 12.0; +} + +class ViewCameraTabPage extends StatefulWidget { + final Map params; + + const ViewCameraTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _ViewCameraTabPageState(params); +} + +class _ViewCameraTabPageState extends State { + final tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.viewCamera)); + final contentKey = UniqueKey(); + static const IconData selectedIcon = Icons.desktop_windows_sharp; + static const IconData unselectedIcon = Icons.desktop_windows_outlined; + + String? peerId; + bool _isScreenRectSet = false; + int? _display; + + var connectionMap = RxList.empty(growable: true); + + _ViewCameraTabPageState(Map params) { + RemoteCountState.init(); + peerId = params['id']; + final sessionId = params['session_id']; + final tabWindowId = params['tab_window_id']; + final display = params['display']; + final displays = params['displays']; + final screenRect = parseParamScreenRect(params); + _isScreenRectSet = screenRect != null; + _display = display as int?; + tryMoveToScreenAndSetFullscreen(screenRect); + if (peerId != null) { + ConnectionTypeState.init(peerId!); + tabController.onSelected = (id) { + final viewCameraPage = tabController.widget(id); + if (viewCameraPage is ViewCameraPage) { + final ffi = viewCameraPage.ffi; + bind.setCurSessionId(sessionId: ffi.sessionId); + } + WindowController.fromWindowId(params['windowId']) + .setTitle(getWindowNameWithId(id)); + UnreadChatCountState.find(id).value = 0; + }; + tabController.add(TabInfo( + key: peerId!, + label: peerId!, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () async { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: peerId!, + tabController: tabController, + )) { + return; + } + tabController.closeBy(peerId!); + }, + page: ViewCameraPage( + key: ValueKey(peerId), + id: peerId!, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: params['password'], + toolbarState: ToolbarState(), + tabController: tabController, + connToken: params['connToken'], + forceRelay: params['forceRelay'], + isSharedPassword: params['isSharedPassword'], + ), + )); + _update_remote_count(); + } + tabController.onRemoved = (_, id) => onRemoveId(id); + rustDeskWinManager.setMethodHandler(_remoteMethodHandler); + } + + @override + void initState() { + super.initState(); + + if (!_isScreenRectSet) { + Future.delayed(Duration.zero, () { + restoreWindowPosition( + WindowType.ViewCamera, + windowId: windowId(), + peerId: tabController.state.value.tabs.isEmpty + ? null + : tabController.state.value.tabs[0].key, + display: _display, + ); + }); + } + } + + @override + Widget build(BuildContext context) { + final child = Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton(), + selectedBorderColor: MyTheme.accent, + pageViewBuilder: (pageView) => pageView, + labelGetter: DesktopTab.tablabelGetter, + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + bool secure = + connectionType.secure.value == ConnectionType.strSecure; + bool direct = + connectionType.direct.value == ConnectionType.strDirect; + String msgConn = getConnectionText( + secure, direct, connectionType.stream_type.value); + var msgFingerprint = '${translate('Fingerprint')}:\n'; + var fingerprint = FingerprintState.find(key).value; + if (fingerprint.isEmpty) { + fingerprint = 'N/A'; + } + if (fingerprint.length > 5 * 8) { + var first = fingerprint.substring(0, 39); + var second = fingerprint.substring(40); + msgFingerprint += '$first\n$second'; + } else { + msgFingerprint += fingerprint; + } + + final tab = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgConn\n$msgFingerprint', + child: SvgPicture.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + unreadMessageCountBuilder(UnreadChatCountState.find(key)) + .marginOnly(left: 4), + ], + ); + + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as ViewCameraPage; + if (viewCameraPage.ffi.ffiModel.pi.isSet.isTrue && + e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return _tabMenuBuilder(key, cancelFunc); + }, + target: e.position, + ); + } + }, + child: tab, + ); + } + }), + ), + ); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : workaroundWindowBorder( + context, + Obx(() => Container( + decoration: BoxDecoration( + border: Border.all( + color: MyTheme.color(context).border!, + width: stateGlobal.windowBorderWidth.value), + ), + child: child, + ))); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : Obx(() => SubWindowDragToResizeArea( + key: contentKey, + child: tabWidget, + // Specially configured for a better resize area and remote control. + childPadding: kDragToResizeAreaPadding, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, + windowId: stateGlobal.windowId, + )); + } + + // Note: Some dup code to ../widgets/remote_toolbar + Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) { + final List> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as ViewCameraPage; + final ffi = viewCameraPage.ffi; + final sessionId = ffi.sessionId; + final toolbarState = viewCameraPage.toolbarState; + menu.addAll([ + MenuEntryButton( + childBuilder: (TextStyle? style) => Obx(() => Text( + translate( + toolbarState.hide.isTrue ? 'Show Toolbar' : 'Hide Toolbar'), + style: style, + )), + proc: () { + toolbarState.switchHide(sessionId); + cancelFunc(); + }, + padding: padding, + ), + ]); + + if (tabController.state.value.tabs.length > 1) { + final splitAction = MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Move tab to new window'), + style: style, + ), + proc: () async { + await DesktopMultiWindow.invokeMethod( + kMainWindowId, + kWindowEventMoveTabToNewWindow, + '${windowId()},$key,$sessionId,ViewCamera'); + cancelFunc(); + }, + padding: padding, + ); + menu.insert(1, splitAction); + } + + menu.addAll([ + MenuEntryDivider(), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Copy Fingerprint'), + style: style, + ), + proc: () => onCopyFingerprint(FingerprintState.find(key).value), + padding: padding, + dismissOnClicked: true, + dismissCallback: cancelFunc, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Close'), + style: style, + ), + proc: () async { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: key, + tabController: tabController, + )) { + return; + } + tabController.closeBy(key); + cancelFunc(); + }, + padding: padding, + ) + ]); + + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenuTheme.blueColor, + height: _MenuTheme.height, + dividerHeight: _MenuTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + void onRemoveId(String id) async { + if (tabController.state.value.tabs.isEmpty) { + // Keep calling until the window status is hidden. + // + // Workaround for Windows: + // If you click other buttons and close in msgbox within a very short period of time, the close may fail. + // `await WindowController.fromWindowId(windowId()).close();`. + Future loopCloseWindow() async { + int c = 0; + final windowController = WindowController.fromWindowId(windowId()); + while (c < 20 && + tabController.state.value.tabs.isEmpty && + (!await windowController.isHidden())) { + await windowController.close(); + await Future.delayed(Duration(milliseconds: 100)); + c++; + } + } + + loopCloseWindow(); + } + ConnectionTypeState.delete(id); + _update_remote_count(); + } + + int windowId() { + return widget.params["windowId"]; + } + + Future handleWindowCloseButton() async { + final connLength = tabController.length; + if (connLength == 1) { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: tabController.state.value.tabs[0].key, + tabController: tabController, + )) { + return false; + } + } + if (connLength <= 1) { + tabController.clear(); + return true; + } else { + final bool res; + if (!option2bool(kOptionEnableConfirmClosingTabs, + bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + tabController.clear(); + } + return res; + } + } + + _update_remote_count() => + RemoteCountState.find().value = tabController.length; + + Future _remoteMethodHandler(call, fromWindowId) async { + debugPrint( + "[View Camera Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + + dynamic returnValue; + // for simplify, just replace connectionId + if (call.method == kWindowEventNewViewCamera) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final sessionId = args['session_id']; + final tabWindowId = args['tab_window_id']; + final display = args['display']; + final displays = args['displays']; + final screenRect = parseParamScreenRect(args); + final prePeerCount = tabController.length; + Future.delayed(Duration.zero, () async { + if (stateGlobal.fullscreen.isTrue) { + await WindowController.fromWindowId(windowId()).setFullscreen(false); + stateGlobal.setFullscreen(false, procWnd: false); + } + await setNewConnectWindowFrame(windowId(), id!, prePeerCount, + WindowType.ViewCamera, display, screenRect); + Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { + await windowOnTop(windowId()); + }); + }); + ConnectionTypeState.init(id); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () async { + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: id, + tabController: tabController, + )) { + return; + } + tabController.closeBy(id); + }, + page: ViewCameraPage( + key: ValueKey(id), + id: id, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: args['password'], + toolbarState: ToolbarState(), + tabController: tabController, + connToken: args['connToken'], + forceRelay: args['forceRelay'], + isSharedPassword: args['isSharedPassword'], + ), + )); + } else if (call.method == kWindowDisableGrabKeyboard) { + // ??? + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } else if (call.method == kWindowEventActiveSession) { + final jumpOk = tabController.jumpToByKey(call.arguments); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventActiveDisplaySession) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final display = args['display']; + final jumpOk = + tabController.jumpToByKeyAndDisplay(id, display, isCamera: true); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventGetRemoteList) { + return tabController.state.value.tabs + .map((e) => e.key) + .toList() + .join(','); + } else if (call.method == kWindowEventGetSessionIdList) { + return tabController.state.value.tabs + .map((e) => '${e.key},${(e.page as ViewCameraPage).ffi.sessionId}') + .toList() + .join(';'); + } else if (call.method == kWindowEventGetCachedSessionData) { + // Ready to show new window and close old tab. + final args = jsonDecode(call.arguments); + final id = args['id']; + final close = args['close']; + try { + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == id) + .page as ViewCameraPage; + returnValue = viewCameraPage.ffi.ffiModel.cachedPeerData.toString(); + } catch (e) { + debugPrint('Failed to get cached session data: $e'); + } + if (close && returnValue != null) { + closeSessionOnDispose[id] = false; + tabController.closeBy(id); + } + } else if (call.method == kWindowEventRemoteWindowCoords) { + final viewCameraPage = + tabController.state.value.selectedTabInfo.page as ViewCameraPage; + final ffi = viewCameraPage.ffi; + final displayRect = ffi.ffiModel.displaysRect(); + if (displayRect != null) { + final wc = WindowController.fromWindowId(windowId()); + Rect? frame; + try { + frame = await wc.getFrame(); + } catch (e) { + debugPrint( + "Failed to get frame of window $windowId, it may be hidden"); + } + if (frame != null) { + ffi.cursorModel.moveLocal(0, 0); + final coords = RemoteWindowCoords( + frame, + CanvasCoords.fromCanvasModel(ffi.canvasModel), + CursorCoords.fromCursorModel(ffi.cursorModel), + displayRect); + returnValue = jsonEncode(coords.toJson()); + } + } + } else if (call.method == kWindowEventSetFullscreen) { + stateGlobal.setFullscreen(call.arguments == 'true'); + } + _update_remote_count(); + return returnValue; + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart new file mode 100644 index 0000000..f766033 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/file_manager_tab_page.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: Scaffold( + backgroundColor: isLinux ? Colors.transparent : null, + body: FileManagerTabPage( + params: params, + ), + ), + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_port_forward_screen.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_port_forward_screen.dart new file mode 100644 index 0000000..c586a58 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_port_forward_screen.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/port_forward_tab_page.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab file port forward screen +class DesktopPortForwardScreen extends StatelessWidget { + final Map params; + + const DesktopPortForwardScreen({Key? key, required this.params}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ], + child: Scaffold( + backgroundColor: isLinux ? Colors.transparent : null, + body: PortForwardTabPage( + params: params, + ), + ), + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_remote_screen.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_remote_screen.dart new file mode 100644 index 0000000..e88078e --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab desktop remote screen +class DesktopRemoteScreen extends StatelessWidget { + final Map params; + + DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) { + bind.mainInitInputSource(); + stateGlobal.getInputSource(force: true); + } + + @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: Scaffold( + // Set transparent background for padding the resize area out of the flutter view. + // This allows the wallpaper goes through our resize area. (Linux only now). + backgroundColor: isLinux ? Colors.transparent : null, + body: ConnectionTabPage( + params: params, + ), + )); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_terminal_screen.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_terminal_screen.dart new file mode 100644 index 0000000..301489c --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_terminal_screen.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:provider/provider.dart'; + +import 'package:flutter_hbb/desktop/pages/terminal_tab_page.dart'; + +class DesktopTerminalScreen extends StatelessWidget { + final Map params; + + const DesktopTerminalScreen({Key? key, required this.params}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ], + child: Scaffold( + backgroundColor: isLinux ? Colors.transparent : null, + body: TerminalTabPage( + params: params, + ), + ), + ); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_view_camera_screen.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_view_camera_screen.dart new file mode 100644 index 0000000..a845b89 --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/screen/desktop_view_camera_screen.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_tab_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab desktop remote screen +class DesktopViewCameraScreen extends StatelessWidget { + final Map params; + + DesktopViewCameraScreen({Key? key, required this.params}) : super(key: key) { + bind.mainInitInputSource(); + stateGlobal.getInputSource(force: true); + } + + @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: Scaffold( + // Set transparent background for padding the resize area out of the flutter view. + // This allows the wallpaper goes through our resize area. (Linux only now). + backgroundColor: isLinux ? Colors.transparent : null, + body: ViewCameraTabPage( + params: params, + ), + )); + } +} diff --git a/shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/button.dart b/shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/button.dart new file mode 100644 index 0000000..0c09f7c --- /dev/null +++ b/shelled/rustdesk-as-ref/flutter/lib/desktop/widgets/button.dart @@ -0,0 +1,171 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +class Button extends StatefulWidget { + final GestureTapCallback onTap; + final String text; + final double? textSize; + final double? minWidth; + final bool isOutline; + final double? padding; + final Color? textColor; + final double? radius; + final Color? borderColor; + + Button({ + Key? key, + this.minWidth, + this.isOutline = false, + this.textSize, + this.padding, + this.textColor, + this.radius, + this.borderColor, + required this.onTap, + required this.text, + }) : super(key: key); + + @override + State : "" } + {auth && !disconnected && show_elevation_btn ? : "" } +
+ {!auth && show_accept_btn ? : "" } + {!auth ? : "" } +
+ {auth && !disconnected ? : "" } + {auth && disconnected ? : "" } + + {c.is_file_transfer || c.is_terminal || c.port_forward ? "" :
{svg_chat}
} + +
+ {c.is_file_transfer || c.is_terminal || c.port_forward ? "" : } +
+ ; + } + + function sendMsg(text) { + if (!text) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.msgs.push({ name: "me", text: text, time: getNowStr()}); + handler.send_msg(cid, text); + body.update(); + }); + } + + event click $(icon.keyboard) (e) { + var { cid, connection } = this; + checkClickTime(function() { + connection.keyboard = !connection.keyboard; + body.update(); + handler.switch_permission(cid, "keyboard", connection.keyboard); + }); + } + + event click $(icon.clipboard) { + var { cid, connection } = this; + checkClickTime(function() { + connection.clipboard = !connection.clipboard; + body.update(); + handler.switch_permission(cid, "clipboard", connection.clipboard); + }); + } + + event click $(icon.audio) { + var { cid, connection } = this; + checkClickTime(function() { + connection.audio = !connection.audio; + body.update(); + handler.switch_permission(cid, "audio", connection.audio); + }); + } + + event click $(icon.file) { + var { cid, connection } = this; + checkClickTime(function() { + connection.file = !connection.file; + body.update(); + handler.switch_permission(cid, "file", connection.file); + }); + } + + event click $(icon.restart) { + var { cid, connection } = this; + checkClickTime(function() { + connection.restart = !connection.restart; + body.update(); + handler.switch_permission(cid, "restart", connection.restart); + }); + } + + event click $(icon.recording) { + var { cid, connection } = this; + checkClickTime(function() { + connection.recording = !connection.recording; + body.update(); + handler.switch_permission(cid, "recording", connection.recording); + }); + } + + event click $(icon.block_input) { + var { cid, connection } = this; + checkClickTime(function() { + connection.block_input = !connection.block_input; + body.update(); + handler.switch_permission(cid, "block_input", connection.block_input); + }); + } + + event click $(button#accept) { + var { cid, connection } = this; + checkClickTime(function() { + connection.authorized = true; + body.update(); + handler.authorize(cid); + self.timer(30ms, function() { + setWindowState(View.WINDOW_MINIMIZED); + }); + }); + } + + event click $(button#elevate_accept) { + var { cid, connection } = this; + checkClickTime(function() { + connection.authorized = true; + show_elevation = false; + body.update(); + handler.elevate_portable(cid); + handler.authorize(cid); + self.timer(30ms, function() { + setWindowState(View.WINDOW_MINIMIZED); + }); + }); + } + + event click $(button#elevate) { + var { cid, connection } = this; + checkClickTime(function() { + show_elevation = false; + body.update(); + handler.elevate_portable(cid); + self.timer(30ms, function() { + setWindowState(View.WINDOW_MINIMIZED); + }); + }); + } + + event click $(button#dismiss) { + var cid = this.cid; + checkClickTime(function() { + handler.close(cid); + }); + } + + event click $(button#disconnect) { + var cid = this.cid; + checkClickTime(function() { + handler.close(cid); + }); + } + + event click $(button#close) { + var cid = this.cid; + if (this.cur >= 0 && this.cur < connections.length){ + handler.remove_disconnected_connection(cid); + connections.splice(this.cur, 1); + if (connections.length > 0) { + if (this.cur > 0) + this.cur -= 1; + else + this.cur = connections.length - 1; + header.update(); + body.update(); + } else { + handler.quit(); + } + } + + } +} + +$(body).content(); + +var header; + +class Header: Reactor.Component +{ + function this() { + header = this; + } + + function render() { + var me = this; + var conn = connections[body.cur]; + if (conn && conn.unreaded > 0) {; + var el = me.select("#unreaded" + conn.id); + if (el) el.style.set { + display: "inline-block", + }; + self.timer(300ms, function() { + conn.unreaded = 0; + var el = me.select("#unreaded" + conn.id); + if (el) el.style.set { + display: "none", + }; + }); + } + var tabs = connections.map(function(c, i) { return me.renderTab(c, i) }); + return
+ {tabs} +
+
+ < + > +
+
; + } + + function renderTab(c, i) { + var cur = body.cur; + return
+ {c.name} + {c.unreaded > 0 ? {c.unreaded} : ""} +
; + } + + function update_cur(idx) { + checkClickTime(function() { + body.cur = idx; + update(); + self.timer(1ms, adjustHeader); + }); + } + + event click $(div.tab) (_, me) { + var idx = me.index; + if (idx == body.cur) return; + this.update_cur(idx); + } + + event click $(span#left-arrow) { + var cur = body.cur; + if (cur == 0) return; + this.update_cur(cur - 1); + } + + event click $(span#right-arrow) { + var cur = body.cur; + if (cur == connections.length - 1) return; + this.update_cur(cur + 1); + } +} + +if (is_osx) { + $(header).content(
); + $(header).attributes["role"] = "window-caption"; +} else { + $(div.window-toolbar).content(
); + setWindowButontsAndIcon(true); +} + +event click $(div.chaticon) { + checkClickTime(function() { + show_chat = !show_chat; + adaptSizeForChat(); + if (show_chat) { + view.focus = $(.outline-focus); + } + }); +} + +function checkClickTime(callback) { + var click_callback_time = getTime(); + handler.check_click_time(body.cid); + self.timer(120ms, function() { + var d = click_callback_time - handler.get_click_time(); + if (d > 120) + callback(); + }); +} + +function adaptSizeForChat() { + $(div.right-panel).style.set { + display: show_chat ? "block" : "none", + }; + var (x, y, w, h) = view.box(#rectw, #border, #screen); + if (show_chat && w < scaleIt(600)) { + view.move(x - (scaleIt(600) - w), y, scaleIt(600), h); + } else if (!show_chat && w > scaleIt(450)) { + view.move(x + (w - scaleIt(300)), y, scaleIt(300), h); + } +} + +function update() { + header.update(); + body.update(); +} + +function bring_to_top(idx=-1) { + if (view.windowState == View.WINDOW_HIDDEN || view.windowState == View.WINDOW_MINIMIZED) { + if (is_linux) { + view.focus = self; + } else { + setWindowState(View.WINDOW_SHOWN); + } + if (idx >= 0) body.cur = idx; + } else { + view.windowTopmost = true; + view.windowTopmost = false; + } +} + +handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { + stdout.println("new connection #" + id + ": " + peer_id); + var conn; + connections.map(function(c) { + if (c.id == id) conn = c; + }); + if (conn) { + conn.authorized = authorized; + update(); + return; + } + var idx = -1; + connections.map(function(c, i) { + if (c.disconnected && c.peer_id == peer_id) idx = i; + }); + if (!name) name = "NA"; + conn = { + id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id, + port_forward: port_forward, + avatar: avatar, + name: name, authorized: authorized, time: new Date(), now: new Date(), + keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, + audio: audio, file: file, restart: restart, recording: recording, + block_input:block_input, + disconnected: false + }; + if (idx < 0) { + connections.push(conn); + body.cur = connections.length - 1; + } else { + connections[idx] = conn; + body.cur = idx; + } + bring_to_top(); + update(); + self.timer(1ms, adjustHeader); + if (authorized) { + self.timer(3s, function() { + setWindowState(View.WINDOW_MINIMIZED); + }); + } +} + +handler.removeConnection = function(id, close) { + var i = -1; + connections.map(function(c, idx) { + if (c.id == id) i = idx; + }); + if (i < 0) return; + if (close) { + connections.splice(i, 1); + } else { + var conn = connections[i]; + conn.disconnected = true; + } + if (connections.length > 0) { + if (body.cur >= i && body.cur > 0 && close) body.cur -= 1; + update(); + } +} + +handler.newMessage = function(id, text) { + var idx = -1; + connections.map(function(c, i) { + if (c.id == id) idx = i; + }); + var conn = connections[idx]; + if (!conn) return; + conn.msgs.push({name: conn.name, text: text, time: getNowStr()}); + bring_to_top(idx); + if (idx == body.cur) { + var shouldAdapt = !show_chat; + show_chat = true; + if (shouldAdapt) adaptSizeForChat(); + } + conn.unreaded += 1; + update(); +} + +handler.showElevation = function(show) { + if (show != show_elevation) { + show_elevation = show; + update(); + } +} + +view << event statechange { + adjustBorder(); +} + +function self.ready() { + adjustBorder(); + var (sw, sh) = view.screenBox(#workarea, #dimension); + var w = scaleIt(300); + var h = scaleIt(400); + view.move(sw - w, 0, w, h); +} + +function getElapsed(time, now) { + var seconds = Date.diff(time, now, #seconds); + var hours = seconds / 3600; + var days = hours / 24; + hours = hours % 24; + var minutes = seconds % 3600 / 60; + seconds = seconds % 60; + var out = String.printf("%02d:%02d:%02d", hours, minutes, seconds); + if (days > 0) { + out = String.printf("%d day%s %s", days, days > 1 ? "s" : "", out); + } + return out; +} + +var ui_status_cache = [""]; +function check_update_ui() { + self.timer(1s, function() { + var approve_mode = handler.get_option('approve-mode'); + var changed = false; + if (ui_status_cache[0] != approve_mode) { + ui_status_cache[0] = approve_mode; + changed = true; + } + if (changed) update(); + check_update_ui(); + }); +} +check_update_ui(); + +function updateTime() { + self.timer(1s, function() { + var now = new Date(); + connections.map(function(c) { + if (!c.authorized) c.time = now; + if (!c.disconnected) c.now = now; + }); + var el = $(#time); + if (el) { + var c = connections[body.cur]; + if (c && c.authorized && !c.disconnected) { + el.text = getElapsed(c.time, c.now); + } + } + updateTime(); + }); +} + +updateTime(); + +var tm0 = getTime(); + +function self.closing() { + if (connections.length == 0 && getTime() - tm0 > 30000) return true; + setWindowState(View.WINDOW_HIDDEN); + return false; +} + + +function adjustHeader() { + var hw = $(header).box(#width) / scaleFactor; + var tabswrapper = $(div.tabs-wrapper); + var tabs = $(div.tabs); + var arrows = $(div.tab-arrows); + if (!arrows) return; + var n = connections.length; + var wtab = 80; + var max = hw - 98; + var need_width = n * wtab + scaleIt(2); // include border of active tab + if (need_width < max) { + arrows.style.set { + display: "none", + }; + tabs.style.set { + width: need_width, + margin-left: 0, + }; + tabswrapper.style.set { + width: need_width, + }; + } else { + var margin = (body.cur + 1) * wtab - max + 30; + if (margin < 0) margin = 0; + arrows.style.set { + display: "block", + }; + tabs.style.set { + width: (max - 20 + margin) + 'px', + margin-left: -margin + 'px' + }; + tabswrapper.style.set { + width: (max + 10) + 'px', + }; + } +} + +view.on("size", adjustHeader); + +// handler.addConnection(0, false, false, 0, "", "test1", true, false, false, true, true); +// handler.addConnection(1, false, false, 0, "", "test2--------", true, false, false, false, false); +// handler.addConnection(2, false, false, 0, "", "test3", true, false, false, false, false); +// handler.newMessage(0, 'h'); diff --git a/shelled/rustdesk-as-ref/src/ui/common.css b/shelled/rustdesk-as-ref/src/ui/common.css new file mode 100644 index 0000000..16dd6ca --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/common.css @@ -0,0 +1,492 @@ +html { + var(accent): #0071ff; + var(button): #2C8CFF; + var(gray-bg): #eee; + var(bg): white; + var(border): #ccc; + var(hover-border): #999; + var(text): #222; + var(placeholder): #aaa; + var(lighter-text): #888; + var(light-text): #666; + var(menu-hover): #D7E4F2; + var(dark-red): #A72145; + var(dark-yellow): #FBC732; + var(dark-blue): #2E2459; + var(green-blue): #197260; + var(gray-blue): #2B3439; + var(blue-green): #4299bf; + var(light-green): #D4EAB7; + var(dark-green): #5CB85C; + var(blood-red): #F82600; + var(gray-bg-osx): rgba(238, 238, 238, 0.75); +} + +html.darktheme { + var(bg): #252525; + var(gray-bg): #141414; + var(menu-hover): #2D3033; + var(border): #555; + + var(text): white; + var(light-text): #999; + var(lighter-text): #777; + var(placeholder): #555; + var(gray-bg-osx): rgba(37, 37, 37, 0.75); +} + +body { + margin: 0; + color: color(text); +} + +button.button { + height: 2em; + border-radius: 0.5em; + background: color(button); + color: color(bg); + border-color: color(button); + min-width: 40px; +} + +button[type=checkbox], button[type=checkbox]:active { + background: none; + border: none; + color: unset; + height: 1.4em; +} + +button.outline { + border: color(border) solid 1px; + background: transparent; + color: color(text); +} + +button.button:active, button.active { + background: color(accent); + color: color(bg); + border-color: color(accent); +} + +button.button:hover, button.outline:hover { + border-color: color(hover-border); +} + +button:disabled, +button:disabled:hover { + opacity: 0.3; +} + +button.link { + background: none !important; + border: none; + padding: 0 !important; + color: color(button); + text-decoration: underline; + cursor: pointer; +} + +input[type=text], input[type=password], input[type=number] { + width: *; + font-size: 1.5em; + border-color: color(border); + border-radius: 0; + color: color(text); + padding-left: 0.5em; + background: color(bg); +} + +input:empty { + color: color(placeholder); +} + +input.outline-focus:focus { + outline: color(button) solid 3px; +} + +textarea { + background: color(bg); + color: color(text); +} + +textarea:empty { + color: color(placeholder); +} + +@set my-scrollbar +{ + .prev { display:none; } + .next { display:none; } + .base, .next-page, .prev-page { background: white;} + .slider { background: #bbb; border: white solid 4px; } + .base:disabled { background: transparent; } + .slider:hover { background: grey; } + .slider:active { background: grey; } + .base { size: 16px; } + .corner { background: white; } +} + +@mixin ELLIPSIS { + text-overflow: ellipsis; + white-space: nowrap; + overflow-x: hidden; +} + +.ellipsis { + @ELLIPSIS; +} + +div.password svg:not(.checkmark) { + padding-left: 1em; + size: 16px; + color: #ddd; + background: none; + padding-top: 4px!important; +} + +div.password input { + font-family: Consolas, Menlo, Monaco, 'Courier New'; + font-size: 1.2em; +} + +div.username input { + font-size: 1.2em; +} + +svg { + background: none; +} + +header { + border-bottom: color(border) solid 1px; + height: 22px; + flow: horizontal; + overflow-x: hidden; + position: relative; +} + +@media platform == "OSX" { + header { + background: linear-gradient(top,#E4E4E4,#D1D1D1); + } +} + +header div.window-icon { + size: 22px; +} + +@media platform != "OSX" { +header { + background: white; + height: 30px; +} + +header div.window-icon { + size: 30px; +} +} + +header div.window-icon icon { + display: block; + margin: *; + size: 16px; + background-size: cover; + background-repeat: no-repeat; +} + +header caption { + size: *; +} + +@media platform != "OSX" { + button.window { + top: 0; + padding: 0 10px; + width: 22px; + position: absolute; + color: black; + border: none; + background: none; + border-radius: 0; + } + button.window div { + size: 10px; + margin: *; + background-size: cover; + background-repeat: no-repeat; + } + button.window:hover { + background: color(gray-bg); + } + button.window#minimize { + right: 84px; + } + button.window#maximize { + right: 42px; + } + button.window#close { + right: 0px; + } + button.window#minimize div { + height: 3px; + border-bottom: black solid 1px; + width: 12px; + } + button.window#maximize div { + border: black solid 1px; + } + button.window#close:hover { + background: #F82600; + } + button.window#close:hover div { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAD///////////////+PQt5oAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC'); + } + button.window#close div { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAAAAAAAAAAAAAAAAABPDueNAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC'); + size: 12px; + } + button.window#maximize.restore div { + border: none; + size: 12px; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAQMAAABsu86kAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAB1JREFUCNdjsP/AoCDA8P8CQ0MABipgaHBg+H8AAMfSC36WAZteAAAAAElFTkSuQmCC'); +} +} + +div.chatbox { + size: *; +} + +div.chatbox div.send svg { + size: 16px; +} + +div.chatbox div.send span:active { + opacity: 0.5; +} + +div.chatbox div.send span { + display: inline-block; + padding: 6px; +} + +div.chatbox .msgs { + border: none; + size: *; + border-bottom: color(border) 1px solid; + overflow-x: hidden; + overflow-y: scroll-indicator; + border-spacing: 1em; + padding: 1em; +} + +div.chatbox div.send { + flow: horizontal; + height: 30px; + padding: 5px; +} + +div.chatbox div.send input { + height: 20px !important; +} + +div.chatbox div.name { + color: color(dark-green); +} + +div.chatbox div.right-side div { + text-align: right; +} + +div.chatbox div.text { + margin-top: 0.5em; +} + +@media platform != "OSX" { +header .window-toolbar { + width: max-content; + background: transparent; + position: absolute; + bottom: 4px; + height: 24px; +} +} + +header svg, menu svg { + size: 14px; +} + +header span, menu span { + padding: 4px 8px; + margin: * 0.5em; + color: color(light-text); +} + +progress { + display: inline-block; + aspect: Progress; + border: none; + margin-right: 1em; + height: 0.25em; + background: transparent; +} + +menu { + background: color(bg); +} + +menu div.separator { + height: 1px; + width: *; + margin: 5px 0; + background: color(gray-bg); + border: none; +} + +menu li { + color: color(text); + position: relative; +} + +menu li span { + display: none; +} + +menu li.selected span:nth-child(1) { + display: inline-block; + position: absolute; + left: -10px; + top: 2px; +} + +.link { + cursor: pointer; + text-decoration: underline; +} + +.link:active { + opacity: 0.5; +} + +menu li:hover { + background: color(menu-hover); + color: color(text); +} + +menu li.line-through, menu li.line-through :hover { + text-decoration-line: line-through; + color: red; +} + +#tags { + size: *; + padding: 0.5em; + overflow-y: scroll-indicator; + border-spacing: 0.5em; + flow: horizontal-flow; +} + +#tags span { + background: color(gray-bg); + display: inline-block; + border-radius: 6px; + padding: 3px 0.5em; + word-wrap: normal; +} + +#tags span.active { + background: color(button); + border-color: color(button); + color: white; +} + +#tags span:hover { + border-color: color(hover-border); +} + +div#msgbox .msgbox-icon svg { + size: 80px; + background: white; + +} + +div#msgbox .form { + border-spacing: 0.5em; +} + +div#msgbox .caption { + @ELLIPSIS; + height: 2em; + line-height: 2em; + text-align: center; + color: color(bg); + font-weight: bold; +} + +div#msgbox .form .text { + @ELLIPSIS; +} + +div#msgbox button.button { + margin-left: 1.6em; +} + +div#msgbox div.password { + position: relative; +} + +div#msgbox div.password svg { + position: absolute; + right: 0.25em; + top: 0.25em; + padding: 0.5em; + color: color(text); +} + +div#msgbox div.set-password > div { + flow: horizontal; +} + +div#msgbox div.set-password > div > span { + width: 30%; + line-height: 2em; +} + +div#msgbox div.set-password div.password { + width: *; +} + +div#msgbox div.set-password div > input { + width: *; +} + +div#msgbox div.set-password input { + font-size: 1em; +} + +.wrap-text { + width: *; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; + height: auto; + overflow: hidden; +} + +div#msgbox #error { + color: red; +} + +div.user-session .title { + font-size: 1.2em; + margin-bottom: 2em; +} + +div.user-session select { + width: 98%; + height: 2em; + border-radius: 0.5em; + border: color(border) solid 1px; + background: color(bg); + color: color(text); + padding-left: 0.5em; +} diff --git a/shelled/rustdesk-as-ref/src/ui/common.tis b/shelled/rustdesk-as-ref/src/ui/common.tis new file mode 100644 index 0000000..2407990 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/common.tis @@ -0,0 +1,482 @@ +include "sciter:reactor.tis"; + +var handler = $(#handler) || view; +try { view.windowIcon = self.url(handler.get_icon()); } catch(e) {} +var OS = view.mediaVar("platform"); +var is_osx = OS == "OSX"; +var is_win = OS == "Windows"; +var is_linux = OS == "Linux"; +var is_file_transfer; +var is_xfce = false; +try { is_xfce = handler.is_xfce(); } catch(e) {} + +function isEnterKey(evt) { + return (evt.keyCode == Event.VK_ENTER || + (is_osx && evt.keyCode == 0x4C) || + (is_linux && evt.keyCode == 65421)); +} + +function getScaleFactor() { + if (!is_win) return 1; + var s = self.toPixels(10000dip) / 10000.; + return s < 0.000001 ? 1 : s; +} +var scaleFactor = getScaleFactor(); +view << event resolutionchange { + scaleFactor = getScaleFactor(); +} +function scaleIt(x) { + return (x * scaleFactor).toInteger(); +} +stdout.println("scaleFactor", scaleFactor); + +function translate(name) { + try { + return handler.t(name); + } catch(_) { + return name; + } +} + +function hashCode(str) { + var hash = 160 << 16 + 114 << 8 + 91; + for (var i = 0; i < str.length; i += 1) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash % 16777216; +} + +function intToRGB(i, a = 1) { + return 'rgba(' + ((i >> 16) & 0xFF) + ', ' + ((i >> 8) & 0x7F) + + ',' + (i & 0xFF) + ',' + a + ')'; +} + +function string2RGB(s, a = 1) { + return intToRGB(hashCode(s), a); +} + +function getTime() { + var now = new Date(); + return now.valueOf(); +} + +function platformSvg(platform, color) { + platform = (platform || "").toLowerCase(); + if (platform == "linux") { + return + + + + + ; + } + if (platform == "mac os") { + return + + ; + } + if (platform == "android") { + return ; + } + return + + ; +} + +function centerize(w, h) { + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + if (w > sw) w = sw; + if (h > sh) h = sh; + var x = (sx + sw - w) / 2; + var y = (sy + sh - h) / 2; + view.move(x, y, w, h); +} + +function setWindowButontsAndIcon(only_min=false) { + if (only_min) { + $(div.window-buttons).content(
+
+
); + } else { + $(div.window-buttons).content(
+
+
+
+
); + } + $(div.window-icon>icon).style.set { + "background-image": "url('" + handler.get_icon() + "')", + }; +} + +function adjustBorder() { + if (is_osx) { + if (view.windowState == View.WINDOW_FULL_SCREEN) { + $(header).style.set { + display: "none", + }; + } else { + $(header).style.set { + display: "block", + padding: "0", + }; + } + return; + } + if (view.windowState == view.WINDOW_MAXIMIZED) { + self.style.set { + border: "window-frame-width solid transparent", + }; + } else if (view.windowState == view.WINDOW_FULL_SCREEN) { + self.style.set { + border: "none", + }; + } else { + self.style.set { + border: "black solid 1px", + }; + } + var el = $(button#maximize); + if (el) el.attributes.toggleClass("restore", view.windowState == View.WINDOW_MAXIMIZED); + el = $(span#fullscreen); + if (el) el.attributes.toggleClass("active", view.windowState == View.WINDOW_FULL_SCREEN); +} + +var svg_checkmark = ; +var svg_edit = + +; +var svg_eye = + + +; +var svg_send = + +; +var svg_chat = + +; +var svg_keyboard = ; + +function scrollToBottom(el) { + var y = el.box(#height, #content) - el.box(#height, #client); + el.scrollTo(0, y); +} + +function getNowStr() { + var now = new Date(); + return String.printf("%02d:%02d:%02d", now.hour, now.minute, now.second); +} + +/******************** start of chatbox ****************************************/ +class ChatBox: Reactor.Component { + this var msgs = []; + this var callback; + + function this(params) { + if (params) { + this.msgs = params.msgs || []; + this.callback = params.callback; + } + } + + function renderMsg(msg) { + var cls = msg.name == "me" ? "right-side msg" : "left-side msg"; + return
+ {msg.name == "me" ? +
{msg.time + " "} me
: +
{msg.name} {" " + msg.time}
+ } +
{msg.text}
+
; + } + + function render() { + var me = this; + var msgs = this.msgs.map(function(msg) { return me.renderMsg(msg); }); + self.timer(1ms, function() { + scrollToBottom(me.msgs); + }); + return
+ + {msgs} + +
+ + {svg_send} +
+
; + } + + function send() { + var el = this.$(input); + var value = (el.value || "").trim(); + el.value = ""; + if (!value) return; + if (this.callback) this.callback(value); + } + + event keydown $(input) (evt) { + if (!evt.shortcutKey) { + if (isEnterKey(evt)) { + this.send(); + } + } + } + + event click $(div.send span) { + this.send(); + view.focus = $(input); + } +} +/******************** end of chatbox ****************************************/ + +/******************** start of msgbox ****************************************/ +var remember_password = false; +var last_msgbox_tag = ""; +function msgbox(type, title, content, link="", callback=null, height=180, width=500, hasRetry=false, contentStyle="") { + $(body).scrollTo(0, 0); + if (!type) { + closeMsgbox(); + return; + } + var remember = false; + try { remember = handler.get_remember(); } catch(e) {} + var autoLogin = false; + try { autoLogin = handler.get_option("auto-login") != ''; } catch(e) {} + width += is_xfce ? 50 : 0; + height += is_xfce ? 50 : 0; + + if (type.indexOf("input-password") >= 0) { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login("", "", res.password, res.remember); + if (!is_port_forward) { + // Specially handling file transfer for no permission hanging issue (including 60ms + // timer in setPermission. + // For wrong password input hanging issue, we can not use handler.msgbox. + // But how about wrong password for file transfer? + if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in..."); + else msgbox("connecting", "Connecting...", "Logging in..."); + } + }; + } else if (type.indexOf("input-2fa") >= 0) { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.send2fa(res.code, res.trust_this_device || false); + msgbox("connecting", "Connecting...", "Logging in..."); + }; + } else if (type == "session-login" || type == "session-re-login") { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login(res.osusername, res.ospassword, "", false); + if (!is_port_forward) { + if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in..."); + else msgbox("connecting", "Connecting...", "Logging in..."); + } + }; + } else if (type.indexOf("session-login") >= 0) { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login(res.osusername, res.ospassword, res.password, res.remember); + if (!is_port_forward) { + if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in..."); + else msgbox("connecting", "Connecting...", "Logging in..."); + } + }; + } else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) { + callback = function() { view.close(); } + } else if (type == 'wait-remote-accept-nook') { + callback = function (res) { + if (!res) { + view.close(); + return; + } + }; + } + last_msgbox_tag = type + "-" + title + "-" + content + "-" + link; + $(#msgbox).content(); +} + +function connecting() { + handler.msgbox("connecting", "Connecting...", "Connection in progress. Please wait."); +} + +handler.msgbox = function(type, title, text, link = "", hasRetry=false) { + // crash somehow (when input wrong password), even with small time, for example, 1ms + self.timer(60ms, function() { msgbox(type, title, text, link, null, 180, 500, hasRetry); }); +} + +handler.cancel_msgbox = function(tag) { + if (last_msgbox_tag == tag) { + closeMsgbox(); + } +} + +var reconnectTimeout = 1000; +handler.msgbox_retry = function(type, title, text, link, hasRetry) { + handler.msgbox(type, title, text, link, hasRetry); + if (hasRetry) { + self.timer(0, retryConnect); + self.timer(reconnectTimeout, retryConnect); + reconnectTimeout *= 2; + } else { + reconnectTimeout = 1000; + } +} + +function retryConnect(cancelTimer=false) { + if (cancelTimer) self.timer(0, retryConnect); + if (!is_port_forward) connecting(); + handler.reconnect(false); +} +/******************** end of msgbox ****************************************/ + +function Progress() +{ + var _val; + var pos = -0.25; + + function step() { + if( _val !== undefined ) { this.refresh(); return false; } + pos += 0.02; + if( pos > 1.25) + pos = -0.25; + this.refresh(); + return true; + } + + function paintNoValue(gfx) + { + var (w,h) = this.box(#dimension,#inner); + var x = pos * w; + w = w * 0.25; + gfx.fillColor( this.style#color ) + .pushLayer(#inner-box) + .rectangle(x,0,w,h) + .popLayer(); + return true; + } + + this[#value] = property(v) { + get return _val; + set { + _val = undefined; + pos = -0.25; + this.paintContent = paintNoValue; + this.animate(step); + this.refresh(); + } + } + + this.value = ""; +} + +var svg_eye_cross = + + +; + +class PasswordComponent: Reactor.Component { + this var visible = false; + this var value = ''; + this var name = 'password'; + + function this(params) { + if (params && params.value) { + this.value = params.value; + } + if (params && params.name) { + this.name = params.name; + } + } + + function render() { + return
+ + {this.visible ? svg_eye_cross : svg_eye} +
; + } + + event click $(svg) { + var el = this.$(input); + var value = el.value; + var start = el.xcall(#selectionStart) || 0; + var end = el.xcall(#selectionEnd); + this.update({ visible: !this.visible }); + var me = this; + self.timer(30ms, function() { + var el = me.$(input); + view.focus = el; + el.value = value; + el.xcall(#setSelection, start, end); + }); + } +} + +// type: #post, #get, #delete, #put +function httpRequest(url, type, params, _onSuccess, _onError, headers="") { + if (type != #post) { + stderr.println("only post ok"); + } + handler.post_request(url, JSON.stringify(params), headers); + function check_status() { + var status = handler.get_async_job_status(); + if (status == " ") self.timer(0.1s, check_status); + else { + try { + var data = JSON.parse(status || "{}"); + _onSuccess(data); + } catch (e) { + _onError(status, 0); + } + } + } + check_status(); +} + +function isReasonableSize(r) { + var x = r[0]; + var y = r[1]; + var n = scaleIt(3200); + return !(x < -n || x > n || y < -n || y > n); +} + +function awake() { + view.windowState = View.WINDOW_SHOWN; + view.focus = self; +} + +class MultipleSessionComponent extends Reactor.Component { + this var sessions = []; + this var messageText = translate("Please select the session you want to connect to"); + + function this(params) { + if (params && params.sessions) { + this.sessions = params.sessions; + } + } + + function render() { + return
+
{this.messageText}
+ +
; + } +} \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/src/ui/file_transfer.css b/shelled/rustdesk-as-ref/src/ui/file_transfer.css new file mode 100644 index 0000000..7fd4ac7 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/file_transfer.css @@ -0,0 +1,269 @@ +div#file-transfer-wrapper { + size:*; + display: none; +} + +div#file-transfer { + size: *; + margin: 0; + flow: horizontal; + background: color(gray-bg); + padding: 0.5em; +} + +table +{ + font: system; + border: 1px solid color(border); + flow: table-fixed; + prototype: Grid; + size: *; + padding:0; + border-spacing: 0; + overflow-x: auto; + overflow-y: hidden; +} + +table > thead { + behavior: column-resizer; + border-bottom: color(border) solid 1px; +} + +table > tbody { + behavior: select-multiple; + overflow-y: scroll-indicator; + size: *; + background: white; +} + +table th { + background-color: color(gray-bg); +} + +table th +{ + padding: 4px; + foreground-repeat: no-repeat; + foreground-position: 50% 3px auto auto; + border-left: color(border) solid 1px; +} + +table th.sortable[sort=asc] +{ + foreground-image: url(stock:arrow-down); +} + +table th.sortable[sort=desc] +{ + foreground-image: url(stock:arrow-up); +} + +table th:nth-child(1) { + width: 32px; +} + +table th:nth-child(2) { + width: *; +} + +table th:nth-child(3) { + width: *; +} + +table th:nth-child(4) { + width: 45px; +} + +table.has_current thead th:current { + font-weight: bold; +} + +table tr:nth-child(odd) { background-color: white; } /* each odd row */ +table tr:nth-child(even) { background-color: #F4F5F6; } /* each even row */ + +table.has_current tr:current /* current row */ +{ + background-color: color(accent); +} + +table.has_current tbody tr:checked +{ + background-color: color(accent); +} + +table.has_current tbody tr:checked td { + color: highlighttext; +} + +table td +{ + padding: 4px; + text-align: left; + font-size: 1em; + height: 1.4em; + @ELLIPSIS; +} + +table.folder-view td:nth-child(1) { + behavior:shell-icon; +} + +table td:nth-child(3), table td:nth-child(4) { + color: color(lighter-text); + font-size: 0.9em; +} + +table.has_current tr:current td { + color: white; +} + +table td:nth-child(4) { + text-align: right; +} + +section { + size: *; + margin: 1em; + border-spacing: 0.5em; +} + +table td:nth-child(1) { + foreground-repeat: no-repeat; + foreground-position: 50% 50% +} + +div.toolbar { + flow: horizontal; +} + +div.toolbar svg { + size: 16px; +} + +div.toolbar .spacer { + width: *; +} + +div.toolbar > div.button { + padding: 4px 8px; + opacity: 0.66; +} + +div.toolbar > div.button:active { + opacity: 1; + background-color: #ddd; +} + +div.toolbar > div.button:hover { + opacity: 1; +} + +div.toolbar > div.send { + flow: horizontal; + border-spacing: 0.5em; +} + +div.remote > div.send svg { + transform: scale(-1, 1); +} + +div.navbar { + border: color(border) solid 1px; + padding: 4px 0; +} + +select.select-dir { + width: *; + padding: 0 4px; +} + +div.title { + flow: horizontal; + border-spacing: 1em; + position: relative; +} + +div.title svg.computer { + size: 48px; +} + +div.title div { + margin: * 0; + color: color(light-text); +} + +div.title div.platform { + position: absolute; + left: 12px; + top: 7px; +} + +div.title div.platform svg { + size: 24px; +} + +table.job-table tr td { + width: *; + padding: 0.5em 1em; + border-bottom: color(border) 1px solid; + flow: horizontal; + border-spacing: 1em; + height: 3em; + overflow-x: hidden; +} + +table.job-table tr svg { + size: 16px; +} + +table.job-table tr.is_remote svg { + transform: scale(-1, 1); +} + +table.job-table tr.is_remote div.svg_continue svg { + transform: scale(1, 1); +} + +table.job-table tr td div.text { + width: *; + overflow-x: hidden; +} + +table.job-table tr td div.path { + width: *; + color: color(light-text); + @ELLIPSIS; +} + +table.job-table tr:current td div.path { + color: white; +} + +table#port-forward thead tr th { + padding-left: 1em; + size: *; +} + +table#port-forward tr td { + height: 3em; + text-align: left; +} + +table#port-forward input[type=text], table#port-forward input[type=number] { + font-size: 1.2em; +} + +table#port-forward td.right-arrow svg { + size: 1.2em; + transform: rotate(180deg); +} + +table#port-forward td.remove svg { + size: 0.8em; +} + +table#port-forward tr.value td { + padding-left: 1em; + font-size: 1.5em; + color: black; +} diff --git a/shelled/rustdesk-as-ref/src/ui/file_transfer.tis b/shelled/rustdesk-as-ref/src/ui/file_transfer.tis new file mode 100644 index 0000000..1090c01 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/file_transfer.tis @@ -0,0 +1,819 @@ +var remote_home_dir; + +var svg_add_folder = + + +; +var svg_trash = + + + +; +var svg_arrow = + +; +var svg_home = + +; +var svg_refresh = + +; +var svg_cancel = ; +var svg_continue = ; +var svg_computer = + + + +; + +function getSize(type, size) { + if (!size) { + if (type <= 3) return ""; + return "0B"; + } + size = size.toFloat(); + var toFixed = function(size) { + size = (size * 100).toInteger(); + var a = (size / 100).toInteger(); + if (size % 100 == 0) return a; + if (size % 10 == 0) return a + '.' + (size % 10); + var b = size % 100; + if (b < 10) b = '0' + b; + return a + '.' + b; + } + if (size < 1024) return size.toInteger() + "B"; + if (size < 1024 * 1024) return toFixed(size / 1024) + "K"; + if (size < 1024 * 1024 * 1024) return toFixed(size / (1024 * 1024)) + "M"; + return toFixed(size / (1024 * 1024 * 1024)) + "G"; +} + +function getParentPath(is_remote, path) { + var sep = handler.get_path_sep(is_remote); + var res = path.lastIndexOf(sep); + if (res <= 0) return "/"; + return path.substr(0, res); +} + +function getFileName(is_remote, path) { + var sep = handler.get_path_sep(is_remote); + var res = path.lastIndexOf(sep); + return path.substr(res + 1); +} + +function getExt(name) { + if (name.indexOf(".") == 0) { + return ""; + } + var i = name.lastIndexOf("."); + if (i > 0) return name.substr(i + 1); + return ""; +} + +class JobTable: Reactor.Component { + this var jobs = []; + this var job_map = {}; + + function render() { + var me = this; + var rows = this.jobs.map(function(job, i) { return me.renderRow(job, i); }); + return
+ + {rows} + +
; + } + + event click $(svg.cancel) (_, me) { + var job = this.jobs[me.parent.parent.index]; + var id = job.id; + handler.cancel_job(id); + delete this.job_map[id]; + var i = -1; + this.jobs.map(function(job, idx) { + if (job.id == id) i = idx; + }); + this.jobs.splice(i, 1); + this.update(); + var is_remote = job.is_remote; + if (job.type != "del-dir") is_remote = !is_remote; + refreshDir(is_remote); + } + + event click $(svg.continue) (_, me) { + var job = this.jobs[me.parent.parent.parent.index]; + var id = job.id; + this.continueJob(id); + this.update(); + } + + function clearAllJobs() { + this.jobs = []; + this.job_map = {}; + this.update(); + } + + function send(path, is_remote) { + var to; + var show_hidden; + if (is_remote) { + to = file_transfer.local_folder_view.fd.path; + show_hidden = file_transfer.remote_folder_view.show_hidden; + } else { + to = file_transfer.remote_folder_view.fd.path; + show_hidden = file_transfer.local_folder_view.show_hidden; + } + if (!to) return; + to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path); + var id = handler.get_next_job_id(); + this.jobs.push({ type: "transfer", + id: id, path: path, to: to, + include_hidden: show_hidden, + is_remote: is_remote, + is_last: false + }); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + handler.send_files(id, 0, path, to, 0, show_hidden, is_remote); + var self = this; + self.timer(30ms, function() { self.update(); }); + } + + function addJob(id, path, to, file_num, show_hidden, is_remote, auto_start) { + var job = { type: "transfer", + id: id, path: path, to: to, + include_hidden: show_hidden, + is_remote: is_remote, is_last: true, file_num: file_num }; + this.jobs.push(job); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + handler.update_next_job_id(id + 1); + handler.add_job(id, 0, path, to, file_num, show_hidden, is_remote); + if (auto_start) { + this.continueJob(id); + this.update(); + } + stdout.println(JSON.stringify(job)); + } + + function continueJob(id) { + var job = this.job_map[id]; + if (job == null || !job.is_last){ + return; + } + job.is_last = false; + handler.resume_job(job.id, job.is_remote); + } + + function addDelDir(path, is_remote) { + var id = handler.get_next_job_id(); + this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote }); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + this.update(); + } + + function addDelFile(path, is_remote) { + var id = handler.get_next_job_id(); + this.jobs.push({ type: "del-file", id: id, path: path, is_remote: is_remote }); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + this.update(); + } + + function confirmDeletePolling(is_remote) { + for(var i=0;i n) i = n; + var res = i + ' / ' + n + " " + translate("files"); + if (job.total_size > 0) { + var s = getSize(0, job.finished_size); + if (s) s += " / "; + res += ", " + s + getSize(0, job.total_size); + } + // below has problem if some file skipped + var percent = job.total_size == 0 ? 100 : (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger(); + if (job.finished) percent = '100'; + if (percent) res += ", " + percent + "%"; + if (job.finished) { + if (job.err == "skipped") { + res = translate("Skipped") + " " + res; + } else { + res = translate("Finished") + " " + res; + } + } + if (job.speed) res += ", " + getSize(0, job.speed) + "/s"; + return res; + } + + function updateJob(job) { + var el = this.select("div[id=s" + job.id + "]"); + if (el) el.text = this.getStatus(job); + } + + function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) { + var job = this.job_map[id]; + if (job.type == "del-file"){ + job.finished = true; + job.err = err; + refreshDir(job.is_remote); + this.updateJob(job); + return; + } + if (!job) return; + if (file_num < job.file_num) return; + job.file_num = file_num; + var n = job.num_entries || job.entries.length; + job.finished = job.file_num >= n - 1 || err == "cancel" || err == "skipped"; + job.finished_size = finished_size; + job.speed = speed || 0; + job.err = err; + this.updateJob(job); + if (job.type == "del-dir") { + if (job.finished) { + if (!err) { + handler.remove_dir(job.id, job.path, job.is_remote); + refreshDir(job.is_remote); + // Use the job's is_remote; local variable `is_remote` is undefined in this scope. + if (job.is_remote) file_transfer.remote_folder_view.table.resetCurrent(); + else file_transfer.local_folder_view.table.resetCurrent(); + } + } else if (!job.no_confirm) { + handler.confirm_delete_files(id, job.file_num + 1); + } + } else if (job.finished || file_num == -1) { + refreshDir(!job.is_remote); + } + } + + function renderRow(job, i) { + var svg = this.getSvg(job); + return + {svg} +
+
{job.path}
+
{this.getStatus(job)}
+
+
+ {svg_continue} +
+ {svg_cancel} + ; + } +} + +class FolderView : Reactor.Component { + this var fd = {}; + this var history = []; + this var show_hidden = false; + + function sep() { + return handler.get_path_sep(this.is_remote); + } + + function this(params) { + this.is_remote = params.is_remote; + if (this.is_remote) { + this.show_hidden = !!handler.get_option("remote_show_hidden"); + } else { + this.show_hidden = !!handler.get_option("local_show_hidden"); + } + if (!this.is_remote) { + var dir = handler.get_option("local_dir"); + if (dir) { + this.fd = handler.read_dir(dir, this.show_hidden); + if (this.fd) return; + } + this.fd = handler.read_dir(handler.get_home_dir(), this.show_hidden); + } + } + + // sort predicate + function foldersFirst(a, b) { + if (a.type <= 3 && b.type > 3) return -1; + if (a.type > 3 && b.type <= 3) return +1; + if (a.name == b.name) return 0; + return a.name.toLowerCase().lexicalCompare(b.name.toLowerCase()); + } + + function render() + { + return
+ {this.renderTitle()} + {this.renderNavBar()} + {this.renderOpBar()} + {this.renderTable()} +
; + } + + function renderTitle() { + return
+ {svg_computer} +
{platformSvg(handler.get_platform(this.is_remote), "white")}
+
{translate(this.is_remote ? "Remote Computer" : "Local Computer")}
+
+ } + + function renderNavBar() { + return
+
{svg_home}
+
{svg_arrow}
+
{svg_arrow}
+ {this.renderSelect()} +
{svg_refresh}
+
; + } + + function renderSelect() { + return ; + } + + function renderOpBar() { + if (this.is_remote) { + return
+
{svg_send}{translate('Receive')}
+
+
{svg_add_folder}
+
{svg_trash}
+
; + } + return
+
{svg_add_folder}
+
{svg_trash}
+
+
{translate('Send')}{svg_send}
+
; + } + + function get_updated() { + this.table.sortRows(false); + if (this.fd && this.fd.path) this.select_dir.value = this.fd.path; + } + + function renderTable() { + var fd = this.fd; + var entries = fd.entries || []; + var table = this.table; + if (!table || !table.sortBy) { + entries.sort(this.foldersFirst); + } + var me = this; + var path = fd.path; + if (path != "/" && path) { + entries = [{ name: "..", type: 1 }].concat(entries); + } + var rows = entries.map(function(e) { return me.renderRow(e); }); + var id = (this.is_remote ? "remote" : "local") + "-folder-view"; + return + + + + + {rows} + + + +
  • {svg_checkmark}{translate('Show Hidden Files')}
  • + +
    +
    {translate('Name')}{translate('Modified')}{translate('Size')}
    ; + } + + function joinPath(name) { + var path = this.fd.path; + if (path == "/") { + if (this.sep() == "/") return this.sep() + name; + else return name; + } + return path + (path[path.length - 1] == this.sep() ? "" : this.sep()) + name; + } + + function attached() { + var me = this; + this.table.onRowDoubleClick = function (row) { + var type = row[0].attributes["type"]; + if (type > 3) return; + var name = row[1].text; + var path = name == ".." ? getParentPath(me.is_remote, me.fd.path) : me.joinPath(name); + me.table.resetCurrent(); + me.goto(path, true); + } + this.get_updated(); + } + + function goto(path, push) { + if (!path) return; + if (this.sep() == "\\" && path.length == 2) { // windows drive + path += "\\"; + } + if (push) this.pushHistory(); + if (this.is_remote) { + handler.read_remote_dir(path, this.show_hidden); + } else { + var fd = handler.read_dir(path, this.show_hidden); + this.refresh({ fd: fd }); + } + } + + function refresh(data) { + if (!data.fd || !data.fd.path) return; + if (this.is_remote && !remote_home_dir) { + remote_home_dir = data.fd.path; + } + this.update(data); + var me = this; + self.timer(1ms, function() { me.get_updated(); }); + } + + function renderRow(entry) { + var path; + if (this.is_remote) { + path = handler.get_icon_path(entry.type, getExt(entry.name)); + } else { + path = this.joinPath(entry.name); + } + var tm = entry.time ? new Date(entry.time.toFloat() * 1000.).toLocaleString() : 0; + return + + {entry.name} + {tm || ""} + {getSize(entry.type, entry.size)} + ; + } + + event click $(#switch-hidden) { + this.show_hidden = !this.show_hidden; + this.refreshDir(); + } + + event click $(.goup) () { + var path = this.fd.path; + if (!path || path == "/") return; + path = getParentPath(this.is_remote, path); + this.goto(path, true); + } + + event click $(.goback) () { + var path = this.history.pop(); + if (!path) return; + this.goto(path, false); + } + + event click $(.trash) () { + var rows = this.getCurrentRows(); + if (!rows || rows.length == 0) return; + + var delete_dirs = new Array(); + + for (var i = 0; i < rows.length; ++i) { + var row = rows[i]; + + var path = row[0]; + var type = row[1]; + + var new_history = []; + for (var j = 0; j < this.history.length; ++j) { + var h = this.history[j]; + if ((h + this.sep()).indexOf(path + this.sep()) == -1) new_history.push(h); + } + this.history = new_history; + if (type == 1) { + file_transfer.job_table.addDelDir(path, this.is_remote); + } else { + file_transfer.job_table.addDelFile(path, this.is_remote); + } + } + file_transfer.job_table.confirmDeletePolling(this.is_remote); + } + + event click $(.add-folder) () { + var me = this; + msgbox("custom", translate("Create Folder"), "
    \ +
    " + translate("Please enter the folder name") + ":
    \ +
    \ +
    ", "", function(res=null) { + if (!res) return; + if (!res.name) return; + var name = res.name.trim(); + if (!name) return; + if (name.indexOf(me.sep()) >= 0) { + handler.msgbox("custom-error", "Create Folder", "Invalid folder name"); + return; + } + var path = me.joinPath(name); + var id = handler.get_next_job_id(); + handler.create_dir(id, path, me.is_remote); + create_dir_jobs[id] = { is_remote: me.is_remote, path: path }; + }); + } + + function refreshDir() { + this.goto(this.fd.path, false); + } + + event click $(.refresh) () { + this.refreshDir(); + } + + event click $(.home) () { + var path = this.is_remote ? remote_home_dir : handler.get_home_dir(); + if (!path) return; + if (path == this.fd.path) return; + this.goto(path, true); + } + + function getCurrentRow() { + var row = this.table.getCurrentRow(); + if (!row) return; + var name = row[1].text; + if (!name || name == "..") return; + var type = row[0].attributes["type"]; + return [this.joinPath(name), type]; + } + + function getCurrentRows() { + var rows = this.table.getCurrentRows(); + if (!rows || rows.length== 0) return; + + var records = new Array(); + + for (var i = 0; i < rows.length; ++i) { + var name = rows[i][1].text; + if (!name || name == "..") continue; + + var type = rows[i][0].attributes["type"]; + records.push([this.joinPath(name), type]); + } + return records; + } + + event click $(.send) () { + var rows = this.getCurrentRows(); + if (!rows || rows.length == 0) return; + for (var i = 0; i < rows.length; ++i) { + file_transfer.job_table.send(rows[i][0], this.is_remote); + } + } + + event change $(.select-dir) (_, el) { + var x = getTime() - last_key_time; + if (x < 1000) return; + if (this.fd.path != el.value) { + this.goto(el.value, true); + } + } + + event keydown $(.select-dir) (evt, me) { + if (isEnterKey(evt)) { + this.goto(me.value, true); + } + } + + function pushHistory() { + var path = this.fd.path; + if (!path) return; + if (path != this.history[this.history.length - 1]) this.history.push(path); + } +} + +var file_transfer; + +class FileTransfer: Reactor.Component { + function this() { + file_transfer = this; + } + + function render() { + return
    + + + +
    ; + } +} + +function initializeFileTransfer() +{ + $(#file-transfer-wrapper).content(); + $(#video-wrapper).style.set { visibility: "hidden", position: "absolute" }; + $(#file-transfer-wrapper).style.set { display: "block" }; +} + +handler.updateFolderFiles = function(fd) { + // stdout.println("update folder files: " + JSON.stringify(fd)); + fd.entries = fd.entries || []; + if (fd.id > 0) { + var jt = file_transfer.job_table; + var job = jt.job_map[fd.id]; + if (job) { + job.file_num = -1; + job.total_size = fd.total_size; + job.entries = fd.entries; + job.num_entries = fd.num_entries; + file_transfer.job_table.updateJobStatus(job.id); + } + } else { + file_transfer.remote_folder_view.refresh({ fd: fd }); + } +} + +handler.jobProgress = function(id, file_num, speed, finished_size) { + file_transfer.job_table.updateJobStatus(id, file_num, null, speed, finished_size); +} + +handler.jobDone = function(id, file_num = -1) { + var job = create_dir_jobs[id]; + if (job) { + refreshDir(job.is_remote); + return; + } + file_transfer.job_table.updateJobStatus(id, file_num); +} + +handler.jobError = function(id, err, file_num = -1) { + var job = deleting_single_file_jobs[id]; + if (job) { + msgbox("custom-error", "Delete File", err); + return; + } + job = create_dir_jobs[id]; + if (job) { + msgbox("custom-error", "Create Folder", err); + return; + } + if (file_num < 0) { + handler.msgbox("custom-error", "Failed", err); + } + file_transfer.job_table.updateJobStatus(id, file_num, err); +} + +handler.clearAllJobs = function() { + file_transfer.job_table.clearAllJobs(); +} + +handler.addJob = function (id, path, to, file_num, show_hidden, is_remote, auto_start) { // load last job + // stdout.println("restore job: " + is_remote); + file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote,auto_start); +} + +handler.updateTransferList = function () { + file_transfer.job_table.update(); +} + +function refreshDir(is_remote) { + if (is_remote) file_transfer.remote_folder_view.refreshDir(); + else file_transfer.local_folder_view.refreshDir(); +} + +var deleting_single_file_jobs = {}; +var create_dir_jobs = {} + +function confirmDelete(id ,path, is_remote) { + msgbox("custom-skip", "Confirm Delete", "
    \ +
    " + translate('Are you sure you want to delete this file?') + "
    \ + " + path + "
    \ +
    ", "", function(res=null) { + if (!res) { + file_transfer.job_table.updateJobStatus(id, -1, "cancel"); + file_transfer.job_table.cancelDeletePolling(); + } else if (res.skip) { + file_transfer.job_table.updateJobStatus(id, -1, "cancel"); + file_transfer.job_table.confirmDeletePolling(is_remote); + } else { + handler.remove_file(id, path, 0, is_remote); + if (is_remote) file_transfer.remote_folder_view.table.resetCurrent(); + else file_transfer.local_folder_view.table.resetCurrent(); + deleting_single_file_jobs[id] = { is_remote: is_remote, path: path }; + file_transfer.job_table.confirmDeletePolling(is_remote); + } + }); +} + +handler.confirmDeleteFiles = function(id, i, name) { + var jt = file_transfer.job_table; + var job = jt.job_map[id]; + if (!job) return; + var n = job.num_entries; + if (i >= n) return; + var file_path = job.path; + if (name) file_path += handler.get_path_sep(job.is_remote) + name; + msgbox("custom-skip", "Confirm Delete", "
    \ +
    " + translate('Deleting') + " #" + (i + 1) + " / " + n + " " + translate('files') + ".
    \ +
    " + translate('Are you sure you want to delete this file?') + "
    \ + " + file_path + "
    \ +
    " + translate('Do this for all conflicts') + "
    \ +
    ", "", function(res=null) { + if (!res) { + jt.updateJobStatus(id, i - 1, "cancel"); + file_transfer.job_table.cancelDeletePolling(); + } else if (res.skip) { + if (res.remember){ + jt.updateJobStatus(id, i, "cancel"); + } else{ + handler.jobDone(id, i); + } + file_transfer.job_table.confirmDeletePolling(job.is_remote); + } else { + job.no_confirm = res.remember; + if (job.no_confirm){ + handler.set_no_confirm(id); + file_transfer.job_table.confirmDeletePolling(job.is_remote); + } + handler.remove_file(id, file_path, i, job.is_remote); + } + if(i+1 >= n){ + file_transfer.job_table.confirmDeletePolling(job.is_remote); + } + }); +} + +handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical) { + var jt = file_transfer.job_table; + var identical_msg = is_identical ? translate("identical_file_tip"): ""; + msgbox("custom-skip", "Confirm Write Strategy", "
    \ +
    " + translate('Overwrite') + " " + translate('files') + ".
    \ +
    " + translate('This file exists, skip or overwrite this file?') + "
    \ + " + to + "
    \ +
    " + identical_msg + "
    \ +
    " + translate('Do this for all conflicts') + "
    \ +
    ", "", function(res=null) { + if (!res) { + jt.updateJobStatus(id, -1, "cancel"); + handler.cancel_job(id); + } else if (res.skip) { + if (res.remember){ + handler.set_write_override(id,file_num,false,true, is_upload); // + } else { + handler.set_write_override(id,file_num,false,false,is_upload); // + } + } else { + if (res.remember){ + handler.set_write_override(id,file_num,true,true,is_upload); // + } else { + handler.set_write_override(id,file_num,true,false,is_upload); // + } + } + }); +} + +function save_file_transfer_close_state() { + var local_dir = file_transfer.local_folder_view.fd.path || ""; + var local_show_hidden = file_transfer.local_folder_view.show_hidden ? "Y" : ""; + var remote_dir = file_transfer.remote_folder_view.fd.path || ""; + var remote_show_hidden = file_transfer.remote_folder_view.show_hidden ? "Y" : ""; + handler.save_close_state("local_dir", local_dir); + handler.save_close_state("local_show_hidden", local_show_hidden); + handler.save_close_state("remote_dir", remote_dir); + handler.save_close_state("remote_show_hidden", remote_show_hidden); +} diff --git a/shelled/rustdesk-as-ref/src/ui/grid.tis b/shelled/rustdesk-as-ref/src/ui/grid.tis new file mode 100644 index 0000000..6560521 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/grid.tis @@ -0,0 +1,258 @@ +var last_key_time = 0; +var keymap = {}; +for (var (k, v) in Event) { + k = k + "" + if (k[0] == "V" && k[1] == "K") { + keymap[v] = k; + } +} + +class Grid: Behavior { + const TABLE_HEADER_CLICK = 0x81; + const TABLE_ROW_CLICK = 0x82; + const TABLE_ROW_DBL_CLICK = 0x83; + function onHeaderClick(headerCell) + { + this.postEvent(TABLE_HEADER_CLICK, headerCell.index, headerCell); + return true; + } + + function onRowClick(row , reason) + { + this.postEvent(TABLE_ROW_CLICK, row.index, row); + return true; + } + + function onRowDoubleClick(row) + { + this.postEvent(TABLE_ROW_DBL_CLICK, row.index, row); + return true; + } + + function getCurrentRow() + { + return this.$(tbody>tr:current); + } + + function getCurrentRows() + { + return this.$$(tbody>tr:checked); + } + + function getCurrentColumn() + { + return this.$(thead>:current); // return current cell in header row + } + + function resetCurrent() { + var rows = this.getCurrentRows(); + for (var i = 0; i < rows.length; ++i) { + var row = rows[i]; + row.state.current = false; + row.state.checked = false; + } + } + + function setCurrentRow(row, reason = #by_code, doubleClick = false) + { + if (!row) return; + // get previously selected row: + var prev = this.getCurrentRow(); + if (prev) + { + if (prev === row && !doubleClick) return; // already here, nothing to do. + prev.state.current = false; // drop state flag + prev.state.checked = false; // drop state flag + } + row.state.current = true; + row.state.checked = true; + row.scrollToView(); + + if (doubleClick) + this.onRowDoubleClick(row,reason); + else + this.onRowClick(row,reason); + } + + function setCurrentColumn(col) + { + // get previously selected column: + var prev = this.getCurrentColumn(); + if (prev) + { + if (prev === col) return; // already here, nothing to do. + prev.state.current = false; // drop state flag + } + col.state.current = true; // set state flag + col.scrollToView(); + this.onHeaderClick(col); + } + + function sortRows(sortClicked) + { + var col = this.sortBy; + if (!col) return; + var byColumn = col.index; + var nowDesc = (col.attributes["sort"] || "desc") == "desc"; + if (sortClicked) (this.$(thead [sort]) || col).attributes["sort"] = undefined; // drop any other sort order. + var getValue = function(x) { + var value = x.attributes["value"]; + if (value == undefined) return x.text.toLowerCase(); + return value.toFloat(); + } + var sort = function(r1, r2, asc) { + if (r1[1].text == "..") { + return -1; + } + if (r2[1].text == "..") { + return 1; + } + if (!asc) + return getValue(r1[byColumn]) < getValue(r2[byColumn]) ? -1 : 1; + else + return getValue(r1[byColumn]) > getValue(r2[byColumn]) ? -1 : 1; + } + if (nowDesc) + { + if (sortClicked) col.attributes["sort"] = "asc"; + this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? true : false)); + } else { + if (sortClicked) col.attributes["sort"] = "desc"; + this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? false : true)); + } + } + + function attached() + { + assert this.tag == "table" : "wrong element type for grid, table expected"; + this.body = this.$(:root>tbody); + assert this.body : "Grid require element"; + } + + function onMouse(evt) + { + if ((evt.type != Event.MOUSE_DOWN) && (evt.type != Event.MOUSE_DCLICK)) + return false; + + if (!evt.mainButton) + return false; + + // auxiliary function, returns row this target element belongs to + function targetRow(target) { return target.$p(tbody>tr); } + + // auxiliary function, returns row this target element belongs to + function targetHeaderCell(target) { return target.$p(thead>tr>th); } + + if (var row = targetRow(evt.target)) // click on the row + this.setCurrentRow(row, #by_mouse, evt.type == Event.MOUSE_DCLICK); + else if (var headerCell = targetHeaderCell(evt.target)) + { + this.setCurrentColumn(headerCell); // click on the header cell + if (evt.type != Event.MOUSE_DCLICK && headerCell.$is(.sortable)) { + this.sortBy = headerCell; + this.sortRows(true); + } + } + + //return true; // as it is always ours then stop event bubbling + } + + function onFocus(evt) + { + return (evt.type == Event.GOT_FOCUS || evt.type == Event.LOST_FOCUS); + } + + function onKey(evt) + { + last_key_time = getTime(); + if (evt.type != Event.KEY_DOWN) + return false; + + switch(evt.keyCode) + { + case Event.VK_DOWN: + { + var crow = this.getCurrentRow(); + var idx = crow? crow.index + 1 : 0; + if (idx < this.body.length) this.setCurrentRow(this.body[idx],#by_key); + } + return true; + + case Event.VK_UP: + { + var crow = this.getCurrentRow(); + var idx = crow? crow.index - 1 : this.length - 1; + if (idx >= 0) this.setCurrentRow(this.body[idx],#by_key); + } + return true; + + case Event.VK_PRIOR: + { + var y = this.body.scroll(#top) - this.body.scroll(#height); + var r; + for(var i = this.body.length - 1; i >= 0; --i) + { + var pr = r; r = this.body[i]; + if (r.box(#top, #inner, #content) < y) + { + // this row is further than scroll pos - height of scroll area + this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible + return true; + } + } + this.setCurrentRow(r,#by_key); // just in case + } + return true; + case Event.VK_NEXT: + { + var y = this.body.scroll(#top) + 2 * this.body.scroll(#height); + var lastScrollable = this.body.length - 1; + var r; + for(var i = 0; i <= lastScrollable; ++i) + { + var pr = r; r = this.body[i]; + if (r.box(#bottom, #inner, #content) > y) + { + // this row is further than scroll pos - height of scroll area + this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible + return true; + } + } + this.setCurrentRow(r,#by_key); // just in case + } + return true; + + case Event.VK_HOME: + { + if (this.body.length) + this.setCurrentRow(this.body.first,#by_key); + } + return true; + + case Event.VK_END: + { + if (this.body.length) + this.setCurrentRow(this.body.last,#by_key); + } + return true; + } + var char = handler.get_char(keymap[evt.keyCode] || "", evt.keyCode); + if (char) { + var crow = this.getCurrentRow(); + var idx = crow? crow.index + 1 : 0; + while (idx < this.body.length) { + var el = this.body[idx]; + var text = el[1].text; + if (text && text[0].toLowerCase() == char) { + this.setCurrentRow(el, #by_key); + return true; + } + idx += 1; + } + } + if (isEnterKey(evt)) { + this.onRowDoubleClick(this.getCurrentRow()); + } + return false; + } +} diff --git a/shelled/rustdesk-as-ref/src/ui/header.css b/shelled/rustdesk-as-ref/src/ui/header.css new file mode 100644 index 0000000..8fe4086 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/header.css @@ -0,0 +1,97 @@ +header div { + word-wrap: normal; +} + +header #screens { + background: white; + border: #A9A9A9 1px solid; + height: 22px; + border-radius: 4px; + flow: horizontal; + border-spacing: 0.5em; + padding-right: 1em; + position: relative; +} + +header #screen { + text-align: center; + margin: 3px 0; + width: 18px; + height: 14px; + border: color(border) solid 1px; + font-size: 11px; + color: color(light-text); +} + +@media platform == "OSX" { + header #screen { + line-height: 11px; + } +} + +header #secure { + position: absolute; + left: -10px; + top: -2px; +} + +header #secure svg { + size: 18px; +} + +header .remote-id { + width: 80px; + @ELLIPSIS; + padding-left: 30px; + padding-right: 4em; + margin: * 0; +} + +header span:hover { + background: #f7f7f7; +} + +@media platform != "OSX" { +header span:hover { + background: #d9d9d9; +} +} + +header #screen:hover { + background: #d9d9d9; +} + +header #secure:hover { + background: unset; +} + +header span:active, header #screen:active { + color: black; + background: color(gray-bg); +} + +div#global-screens { + position: relative; + margin: 2px 0; +} + +div#global-screens > div { + position: absolute; + border: color(border) solid 1px; + text-align: center; + color: color(light-text); +} + +header #screen.current, div#global-screens > div.current { + background: #666; + color: white; +} + +span#fullscreen.active { + border: color(border) solid 1px; +} + +button:disabled { + opacity: 0.3; +} + diff --git a/shelled/rustdesk-as-ref/src/ui/header.tis b/shelled/rustdesk-as-ref/src/ui/header.tis new file mode 100644 index 0000000..17efe69 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/header.tis @@ -0,0 +1,716 @@ +var pi = handler.get_default_pi(); // peer information +var chat_msgs = []; + +var svg_fullscreen = + +; +var svg_action = ; +var svg_display = + +; +var svg_secure = + +; +var svg_insecure = ; +var svg_insecure_relay = ; +var svg_secure_relay = ; +var svg_recording_off = ; +var svg_recording_on = ; + +var cur_window_state = view.windowState; +function check_state_change() { + if (view.windowState != cur_window_state) { + stateChanged(); + } + self.timer(30ms, check_state_change); +} + +if (is_linux) { + check_state_change(); +} else { + view << event statechange { + stateChanged(); + } +} + +function get_id() { + return handler.get_option('alias') || handler.get_id() +} + +function stateChanged() { + stdout.println('state changed from ' + cur_window_state + ' -> ' + view.windowState); + cur_window_state = view.windowState; + adjustBorder(); + adaptDisplay(); + if (cur_window_state != View.WINDOW_MINIMIZED) { + view.focus = handler; // to make focus away from restore/maximize button, so that enter key work + } + var fs = view.windowState == View.WINDOW_FULL_SCREEN; + var el = $(#fullscreen); + if (el) el.attributes.toggleClass("active", fs); + el = $(#maximize); + if (el) { + el.state.disabled = fs; + } + if (fs) { + $(header).style.set { + display: "none", + }; + } +} + +var header; +var old_window_state = View.WINDOW_SHOWN; + +var is_edit_os_password; +class EditOsPassword: Reactor.Component { + function render() { + return {svg_edit}; + } + + function onMouse(evt) { + if (evt.type == Event.MOUSE_DOWN) { + is_edit_os_password = true; + editOSPassword(); + } + } +} + +function editOSPassword(login=false) { + var p0 = handler.get_option('os-password'); + msgbox("custom-os-password", 'OS Password', p0, "", function(res=null) { + if (!res) return; + var a0 = handler.get_option('auto-login') != ''; + var p = (res.password || '').trim(); + var a = res.autoLogin || false; + if (p == p0 && a == a0) return; + if (p != p0) handler.set_option('os-password', p); + if (a != a0) handler.set_option('auto-login', a ? 'Y' : ''); + if (p && login) { + handler.input_os_password(p, true); + } + }); +} + +var recording = false; + +class Header: Reactor.Component { + this var conn_note = ""; + + function this() { + header = this; + } + + function render() { + var icon_conn; + var title_conn; + if (this.secure_connection && this.direct_connection) { + icon_conn = svg_secure; + title_conn = translate("Direct and encrypted connection"); + } else if (this.secure_connection && !this.direct_connection) { + icon_conn = svg_secure_relay; + title_conn = translate("Relayed and encrypted connection"); + } else if (!this.secure_connection && this.direct_connection) { + icon_conn = svg_insecure; + title_conn = translate("Direct and unencrypted connection"); + } else { + icon_conn = svg_insecure_relay; + title_conn = translate("Relayed and unencrypted connection"); + } + var stream_type = this.stream_type; + if (stream_type == "Relay") { + stream_type = "TCP"; + } + if (stream_type) { + title_conn += " (" + stream_type + ")"; + } + var title = get_id(); + if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")"; + if ((pi.displays || []).length == 0) { + return
    {title}
    ; + } + var screens = pi.displays.map(function(d, i) { + return
    + {i+1} +
    ; + }); + updateWindowToolbarPosition(); + var style = "flow:horizontal;"; + if (is_osx) style += "margin:*"; + self.timer(1ms, updatePrivacyMode); + self.timer(1ms, toggleMenuState); + return
    + {is_osx || is_xfce ? "" : {svg_fullscreen}} +
    + {icon_conn} +
    {get_id()}
    +
    {screens}
    + {this.renderGlobalScreens()} +
    + {svg_chat} + {svg_action} + {svg_display} + {svg_keyboard} + {recording_enabled ? {recording ? svg_recording_on : svg_recording_off} : ""} + {this.renderKeyboardPop()} + {this.renderDisplayPop()} + {this.renderActionPop()} +
    ; + } + + function renderKeyboardPop(){ + const is_map_mode_supported = handler.is_keyboard_mode_supported("map"); + const is_translate_mode_supported = handler.is_keyboard_mode_supported("translate"); + return + +
  • {svg_checkmark}{translate('Legacy mode')}
  • + { is_map_mode_supported &&
  • {svg_checkmark}{translate('Map mode')}
  • } + { is_translate_mode_supported &&
  • {svg_checkmark}{translate('Translate mode')}
  • } + +
    ; + } + + function renderDisplayPop() { + var codecs = handler.alternative_codecs(); + var show_codec = codecs[0] || codecs[1] || codecs[2] || codecs[3]; + + var cursor_embedded = false; + if ((pi.displays || []).length > 0) { + if (pi.displays.length > pi.current_display) { + cursor_embedded = pi.displays[pi.current_display].cursor_embedded; + } + } + + var is_file_copy_paste_supported = false; + if (handler.version_cmp(pi.version, '1.2.4') < 0) { + is_file_copy_paste_supported = is_win && pi.platform == "Windows"; + } else { + is_file_copy_paste_supported = handler.has_file_clipboard() && pi.platform_additions?.has_file_clipboard; + } + + return + +
  • {translate('Adjust Window')}
  • +
    +
  • {svg_checkmark}{translate('Original')}
  • +
  • {svg_checkmark}{translate('Shrink')}
  • +
  • {svg_checkmark}{translate('Stretch')}
  • +
    +
  • {svg_checkmark}{translate('Good image quality')}
  • +
  • {svg_checkmark}{translate('Balanced')}
  • +
  • {svg_checkmark}{translate('Optimize reaction time')}
  • +
  • {svg_checkmark}{translate('Custom')}
  • + {show_codec ?
    +
    +
  • {svg_checkmark}Auto
  • + {codecs[0] ?
  • {svg_checkmark}VP8
  • : ""} +
  • {svg_checkmark}VP9
  • + {codecs[1] ?
  • {svg_checkmark}AV1
  • : ""} + {codecs[2] ?
  • {svg_checkmark}H264
  • : ""} + {codecs[3] ?
  • {svg_checkmark}H265
  • : ""} +
    : ""} +
    + {!cursor_embedded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • } + {
  • {svg_checkmark}{translate('Follow remote cursor')}
  • } + {
  • {svg_checkmark}{translate('Follow remote window focus')}
  • } +
  • {svg_checkmark}{translate('Show quality monitor')}
  • + {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} + {is_file_copy_paste_supported && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} + {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} + {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} + {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + {keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ?
  • {svg_checkmark}{translate('Swap control-command key')}
  • : ""} + {handler.version_cmp(pi.version, '1.2.4') >= 0 ?
  • {svg_checkmark}{translate('True color (4:4:4)')}
  • : ""} + + ; + } + + function renderActionPop() { + return + + {keyboard_enabled ?
  • {translate('OS Password')}
  • : ""} +
  • {translate('Transfer file')}
  • +
  • {translate('TCP tunneling')}
  • + {handler.get_audit_server("conn") &&
  • {translate('Note')}
  • } +
    + {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • {translate('Insert')} Ctrl + Alt + Del
  • : ""} + {restart_enabled && (pi.platform == "Linux" || pi.platform == "Windows" || pi.platform == "Mac OS") ?
  • {translate('Restart remote device')}
  • : ""} + {keyboard_enabled ?
  • {translate('Insert Lock')}
  • : ""} + {keyboard_enabled && pi.platform == "Windows" && pi.sas_enabled ?
  • {translate("Block user input")}
  • : ""} + {handler.is_screenshot_supported() ?
  • {translate('Take screenshot')}
  • : "" } +
  • {translate('Refresh')}
  • + + ; + } + + function renderGlobalScreens() { + if (pi.displays.length < 3) return ""; + var x0 = 9999999; + var y0 = 9999999; + var x = -9999999; + var y = -9999999; + pi.displays.map(function(d, i) { + if (d.x < x0) x0 = d.x; + if (d.y < y0) y0 = d.y; + var dx = d.x + d.width; + if (dx > x) x = dx; + var dy = d.y + d.height; + if (dy > y) y = dy; + }); + var w = x - x0; + var h = y - y0; + var scale = 16. / h; + var screens = pi.displays.map(function(d, i) { + var min_wh = d.width > d.height ? d.height : d.width; + var fs = min_wh * 0.9 * scale; + var style = "width:" + (d.width * scale) + "px;" + + "height:" + (d.height * scale) + "px;" + + "left:" + ((d.x - x0) * scale) + "px;" + + "top:" + ((d.y - y0) * scale) + "px;" + + "font-size:" + fs + "px;"; + if (is_osx) { + style += "line-height:" + fs + "px;"; + } + return
    {i+1}
    ; + }); + + var style = "width:" + (w * scale) + "px; height:" + (h * scale) + "px;"; + return
    + {screens} +
    ; + } + + event click $(#fullscreen) (_, el) { + if (view.windowState == View.WINDOW_FULL_SCREEN) { + if (old_window_state == View.WINDOW_MAXIMIZED) { + view.windowState = View.WINDOW_SHOWN; + } + view.windowState = old_window_state; + } else { + old_window_state = view.windowState; + if (view.windowState == View.WINDOW_MAXIMIZED) { + view.windowState = View.WINDOW_SHOWN; + } + view.windowState = View.WINDOW_FULL_SCREEN; + if (is_linux) { self.timer(150ms, function() { view.windowState = View.WINDOW_FULL_SCREEN; }); } + } + } + + event click $(#chat) { + startChat(); + } + + event click $(#action) (_, me) { + var menu = $(menu#action-options); + me.popup(menu); + } + + event click $(#display) (_, me) { + var menu = $(menu#display-options); + me.popup(menu); + } + + event click $(#keyboard) (_, me) { + var menu = $(menu#keyboard-options); + me.popup(menu); + } + + event click $(span#recording) (_, me) { + header.update(); + handler.record_screen(!recording) + } + + event click $(#screen) (_, me) { + if (pi.current_display == me.index) return; + handler.switch_display(me.index); + } + + event keyup (evt) { + if((pi.displays || []).length > 0 && evt.keyCode == 220) + { + if (pi.displays.length > pi.current_display) + handler.switch_display(pi.current_display + 1); + else + handler.switch_display(1); + } + } + + event click $(#transfer-file) { + handler.transfer_file(); + } + + event click $(#os-password) (evt) { + if (is_edit_os_password) { + is_edit_os_password = false; + return; + } + var p = handler.get_option('os-password'); + if (!p) editOSPassword(true); + else handler.input_os_password(p, true); + } + + event click $(#tunnel) { + handler.tunnel(); + } + + event click $(#note) { + var self = this; + msgbox("custom", "Note",
    + +
    , "", function(res=null) { + if (!res) return; + if (res.text == null || res.text == undefined) return; + self.conn_note = res.text ?? ""; + handler.send_note(res.text); + }, 280); + } + + event click $(#ctrl-alt-del) { + handler.ctrl_alt_del(); + } + + event click $(#restart_remote_device) { + msgbox( + "restart-confirmation", + translate("Restart remote device"), + translate("Are you sure you want to restart") + " " + pi.username + "@" + pi.hostname + "(" + get_id() + ") ?", + "", + function(res=null) { + if (res != null) handler.restart_remote_device(); + } + ); + } + + event click $(#lock-screen) { + handler.lock_screen(); + } + + event click $(#take-screenshot) { + handler.take_screenshot(pi.current_display, ""); + } + + event click $(#refresh) { + // 0 is just a dummy value. It will be ignored by the handler. + handler.refresh_video(0); + } + + event click $(#block-input) { + if (!input_blocked) { + handler.toggle_option("block-input"); + input_blocked = true; + $(#block-input).text = translate("Unblock user input"); + } else { + handler.toggle_option("unblock-input"); + input_blocked = false; + $(#block-input).text = translate("Block user input"); + } + } + + event click $(menu#display-options li) (_, me) { + if (me.id == "custom") { + handle_custom_image_quality(); + } else if (me.id == "privacy-mode") { + togglePrivacyMode(me.id); + } else if (me.id == "show-quality-monitor") { + toggleQualityMonitor(me.id); + } else if (me.id == "i444") { + toggleI444(me.id); + } else if (me.attributes.hasClass("toggle-option")) { + handler.toggle_option(me.id); + toggleMenuState(); + } else if (!me.attributes.hasClass("selected")) { + var type = me.attributes["type"]; + if (type == "image-quality") { + handler.save_image_quality(me.id); + } else if (type == "view-style") { + handler.save_view_style(me.id); + adaptDisplay(); + } else if (type == "codec-preference") { + handler.set_option("codec-preference", me.id); + handler.update_supported_decodings(); + } + toggleMenuState(); + } + } + + event click $(menu#keyboard-options>li) (_, me) { + if (me.id == "legacy") { + handler.save_keyboard_mode("legacy"); + } else if (me.id == "map") { + handler.save_keyboard_mode("map"); + } else if (me.id == "translate") { + handler.save_keyboard_mode("translate"); + } + toggleMenuState() + } +} + +function handle_custom_image_quality() { + var tmp = handler.get_custom_image_quality(); + var bitrate = (tmp[0] || 50); + var extendedBitrate = bitrate > 100; + var maxRate = extendedBitrate ? 2000 : 100; + msgbox("custom-image-quality", "Custom Image Quality", "
    \ +
    x% Bitrate More
    \ +
    ", "", function(res=null) { + if (!res) return; + if (res.id === "extended-slider") { + var slider = res.parent.$(#bitrate-slider) + slider.slider.max = res.checked ? 2000 : 100; + if (slider.value > slider.slider.max) { + slider.value = slider.slider.max; + } + var buddy = res.parent.$(#bitrate-buddy); + buddy.value = slider.value; + return; + } + if (!res.bitrate) return; + handler.save_custom_image_quality(res.bitrate); + toggleMenuState(); + }); +} + +function toggleMenuState() { + var values = []; + var q = handler.get_image_quality(); + if (!q) q = "balanced"; + values.push(q); + var s = handler.get_view_style(); + if (!s) s = "original"; + values.push(s); + var k = handler.get_keyboard_mode(); + values.push(k); + var c = handler.get_option("codec-preference"); + if (!c) c = "auto"; + values.push(c); + for (var el in $$(menu#display-options li)) { + el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); + } + for (var el in $$(menu#keyboard-options>li)) { + el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); + } + for (var id in ["show-remote-cursor", "follow-remote-cursor", "follow-remote-window", "show-quality-monitor", "disable-audio", "enable-file-copy-paste", "disable-clipboard", "lock-after-session-end", "allow_swap_key", "i444"]) { + var el = self.select('#' + id); + if (el) { + var value = handler.get_toggle_option(id); + el.attributes.toggleClass("selected", value); + } + } +} + +if (is_osx) { + $(header).content(
    ); + $(header).attributes["role"] = "window-caption"; +} else { + if (is_file_transfer || is_port_forward) { + $(caption).content(
    ); + } else { + $(div.window-toolbar).content(
    ); + } + setWindowButontsAndIcon(); +} + +if (!(is_file_transfer || is_port_forward)) { + $(header).style.set { + height: "32px", + }; + if (!is_osx) { + $(div.window-icon).style.set { + size: "32px", + }; + } +} + +handler.updatePi = function(v) { + pi = v; + recording = handler.is_recording(); + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + +handler.updateDisplays = function(v) { + pi.displays = v; + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + +handler.setMultipleWindowsSession = function(sessions) { + // It will be covered by other message box if the timer is not used, + self.timer(1000ms, function() { + msgbox("multiple-sessions-nocancel", translate("Multiple Windows sessions found"), , "", function(res) { + if (res && res.sid) { + handler.set_selected_windows_session_id("" + res.sid); + } + }, 230); + }); +} + +handler.setCurrentDisplay = function(v) { + pi.current_display = v; + handler.switch_display(v); + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + +handler.screenshot = function(msg) { + if (msg) { + msgbox( + "custom-nocancel-nook-hasclose-error", + translate("Take screenshot"), + msg, + "", + function() {} + ); + } else { + msgbox( + "custom-take-screenshot-nocancel-nook", + translate("Take screenshot"), + translate("screenshot-action-tip"), + "", + function() {} + ); + } +} + +function updatePrivacyMode() { + var el = $(li#privacy-mode); + if (el) { + var supported = handler.is_privacy_mode_supported(); + if (!supported) { + // el.attributes.toggleClass("line-through", true); + el.style["display"]="none"; + } else { + var value = handler.get_toggle_option("privacy-mode"); + el.attributes.toggleClass("selected", value); + var el = $(li#block-input); + if (el) { + el.state.disabled = value; + } + } + } +} +handler.updatePrivacyMode = updatePrivacyMode; + +function togglePrivacyMode(privacy_id) { + var supported = handler.is_privacy_mode_supported(); + if (!supported) { + msgbox("nocancel", translate("Privacy mode"), translate("Unsupported"), "", function() { }); + } else { + handler.toggle_option(privacy_id); + } +} + +function toggleQualityMonitor(name) { + var show = handler.get_toggle_option(name); + if (show) { + $(#quality-monitor).style.set{ display: "none" }; + } else { + $(#quality-monitor).style.set{ display: "block" }; + } + handler.toggle_option(name); + toggleMenuState(); +} + +function toggleI444(name) { + handler.toggle_option(name); + handler.update_supported_decodings(); + toggleMenuState(); +} + +handler.updateBlockInputState = function(input_blocked) { + if (!input_blocked) { + handler.toggle_option("block-input"); + input_blocked = true; + $(#block-input).text = translate("Unblock user input"); + } else { + handler.toggle_option("unblock-input"); + input_blocked = false; + $(#block-input).text = translate("Block user input"); + } +} + +handler.switchDisplay = function(i) { + pi.current_display = i; + header.update(); +} + +function updateWindowToolbarPosition() { + if (is_osx) return; + self.timer(1ms, function() { + var el = $(div.window-toolbar); + var w1 = el.box(#width, #border); + var w2 = $(header).box(#width, #border); + var x = (w2 - w1) / 2 / scaleFactor; + el.style.set { + left: x + "px", + display: "block", + }; + }); +} + +view.on("size", function() { + // ensure size is done, so add timer + self.timer(1ms, function() { + updateWindowToolbarPosition(); + adaptDisplay(); + }); +}); + +handler.newMessage = function(text) { + chat_msgs.push({text: text, name: pi.username || "", time: getNowStr()}); + startChat(); +} + +function sendMsg(text) { + chat_msgs.push({text: text, name: "me", time: getNowStr()}); + handler.send_chat(text); + if (chatbox) chatbox.refresh(); +} + +var chatbox; +function startChat() { + if (chatbox) { + chatbox.windowState = View.WINDOW_SHOWN; + chatbox.refresh(); + return; + } + var icon = handler.get_icon(); + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + var w = scaleIt(300); + var h = scaleIt(400); + var x = (sx + sw - w) / 2; + var y = sy + scaleIt(80); + var params = { + type: View.FRAME_WINDOW, + x: x, + y: y, + width: w, + height: h, + client: true, + parameters: { msgs: chat_msgs, callback: sendMsg, icon: icon }, + caption: get_id(), + }; + var html = handler.get_chatbox(); + if (html) params.html = html; + else params.url = self.url("chatbox.html"); + chatbox = view.window(params); +} + +handler.setConnectionType = function(secured, direct, stream_type) { + header.update({ + secure_connection: secured, + direct_connection: direct, + stream_type: stream_type, + }); +} + +handler.updateRecordStatus = function(status) { + recording = status; + header.update(); +} \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/src/ui/index.css b/shelled/rustdesk-as-ref/src/ui/index.css new file mode 100644 index 0000000..d23e4f0 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/index.css @@ -0,0 +1,441 @@ +html { + background-color: transparent; +} + +body { + overflow: hidden; +} + +@media platform != "OSX" { + body { + border-top: color(border) solid 1px; + } +} + +.title { + font-size: 1.4em; +} + +.app { + flow: horizontal; + size: *; +} + +.lighter-text { + color: color(lighter-text); + font-size: 0.9em; +} + +.left-pane { + width: 200px; + height: *; + background: color(bg); + border-right: color(border) 1px solid; + position: relative; +} + +#ab .left-pane { + border-radius: 1em; + padding: 1em; +} + +#ab .right-pane { + background: none; +} + +#ab .right-content { + overflow: unset; +} + +.left-pane > div:nth-child(1) { + border-spacing: 1em; + padding: 20px; + padding-bottom: 60px; /* reserve space for bottom connect-status */ +} + +.left-pane > div.connect-status { + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +.left-pane div { + word-wrap: break-word; +} + +div.sessions-bar { + color: color(light-text); + padding-top: 0.5em; + border-top: color(border) solid 1px; + margin-bottom: 1em; + position: relative; + flow: horizontal; +} + +div.sessions-tab span { + display: inline-block; + padding: 6px 8px; + cursor: pointer; + @ELLIPSIS; +} + +div.sessions-tab svg { + size: 14px; +} + +div.sessions-tab span.active { + cursor: default; + border-radius: 3px; + background: color(bg); + color: color(text); +} + +div.search-id { + width: 120px; + padding: 0; + position: relative; + display: inline-block; +} + +div.search-id input { + font-size: 1em; + height: 20px; + border: none; + padding-left: 26px; +} + +div.search-id span { + position: absolute; + top: 0px; + padding: 6px; + color: color(border); +} + +div.search-id svg { + size: 14px; +} + +span.search-icon { + left: 0px; +} + +span.clear-input { + display: none; + right: 0px; +} + +div.search-id:hover span.clear-input { + display: inline-block; +} + +span.clear-input:hover { + color: black; +} + +.your-desktop { + border-spacing: 0.5em; + border-left: color(accent) solid 2px; + padding-left: 0.5em; +} + +.your-desktop input[type=text] { + padding: 0; + border: none; + height: 1.5em; +} + +.your-desktop > div { + color: color(light-text); +} + +.right-pane { + size: *; + background: color(gray-bg); +} + +.right-content { + overflow: scroll-indicator; + padding: 1.6em; + border-spacing: 1.6em; + size: *; + flow: vertical; +} + +@media platform == "OSX" { + .right-pane { + background: color(gray-bg-osx); + } +} + +@mixin CARD { + padding: 1.6em; + border-spacing: 1em; + background: color(bg); + border-radius: 1em; +} + +.card-connect { + @CARD; + width: 320px; +} + +.right-buttons { + text-align: right; +} + +.right-buttons>button { + margin-left: 1.6em; +} + +div.connect-status { + left: 240px; + border-top: color(border) solid 1px; + width: 100%; + padding: 1em; +} + +div.connect-status > span.connect-status-icon { + border-radius: 4px; + width: 8px; + height: 8px; + display: inline-block; + margin-right: 1em; +} + +div.connect-status > span.link { + margin-left: 1em; + display: inline-block; +} + +span.connect-status-1 { + background: #e04f5f; +} + +span.connect-status1 { + background: #32bea6; +} + +span.connect-status0 { + background: #F5853B; +} + +div.recent-sessions-content { + border-spacing: 1em; + flow: horizontal-flow; +} + +div.remote-session { + border-radius: 1em; + height: 140px; + width: 220px; + padding: 0; + position: relative; + border: none; +} + +div.remote-session:hover, div.remote-session-list:hover { + outline: color(button) solid 2px -2px; +} + +div.remote-session .platform { + width: *; + height: 120px; + padding: *; + position: relative; +} + +div.remote-session .platform .username{ + left: 0; + color: #eee; + position: absolute; + bottom: 38px; + font-size: 0.8em; + width: 220px; + text-align: center; +} + +div.remote-session .platform svg { + width: 60px; + height: 60px; + background: none; +} + +div.remote-session-list { + background: color(bg); + width: 220px; + flow: horizontal; +} + +div.remote-session-list .platform { + size: 42px; +} + +div.remote-session-list .platform svg { + width: 30px; + height: 30px; + background: none; + padding: 6px; +} + +div.remote-session-list .name { + size: *; + padding-left: 1em; +} + +div.remote-session-list .name >div { + margin-top: *; + margin-bottom: *; + width: *; +} + +div.remote-session-list .name .username { + margin-top: 3px; + font-size: 0.8em; + color: color(lighter-text); +} + +div.remote-session .text { + background: color(bg); + position: absolute; + height: 3em; + width: 100%; + border-radius: 0 0 1em 1em; + bottom: 0; + flow: horizontal; +} + +div.remote-session .text > div { + padding-top: 1em; + padding-left: 1em; + width: *; +} + +svg#menu { + size: 1em; + background: none; + padding: 0.5em; + margin: 0.5em; + color: color(light-text); +} + +.remote-session-list svg#menu { + margin-right: 0; +} + +svg#menu:hover { + color: color(text); + border-radius: 1em; + background: color(gray-bg); +} + +svg#edit:hover { + color: color(text); +} + +svg#edit { + display: inline-block; +} + +div.install-me, div.trust-me { + margin-top: 0.5em; + padding: 20px; + color: white; + background: linear-gradient(left,#e242bc,#f4727c); +} + +div.trust-me > div:nth-child(1), +div.install-me > div:nth-child(1) { + font-size: 1.2em; + font-weight: bold; + text-align: center; + margin-bottom: 0.5em; +} + +div.install-me > div:nth-child(2) { + line-height: 1.4em; +} + +#install-me.link { + margin-top: 0.5em; +} + +div.trust-me > div:nth-child(2) { + font-size: 0.9em; + margin-bottom: 1em; +} + +div.install-me > div:nth-child(3), +div.trust-me > div:nth-child(3) { + text-align: center; + font-size: 1.5em; + font-weight: bold; +} + +div.trust-me > div:nth-child(4), +div.trust-me > div:nth-child(5) { + margin-top: 0.5em; + text-align: center; +} + +div#myid, div#tags-label { + position: relative; +} + +div#myid svg#menu, div#tags-label svg#menu { + position: absolute; + right: -1em; +} + +div#tags-label svg#menu:hover { + background-color: #ddd; +} + +div.remote-session svg#menu { + position: absolute; + right: 0; + top: 0; +} + +.install-me .button { + height: 2em; + line-height: 2em; + text-align: center; + font-weight: bold; + font-size: 1em; + margin-top: 1em; + border-color: white; + border: 1px; + background: none; + color: white; +} + +svg#refresh-password { + display: inline-block; + stroke:#ddd; +} + +svg#refresh-password:hover { + stroke:color(text); +} + +li:disabled, li:disabled:hover { + color: color(lighter-text); + background: color(menu); + opacity: 0.8; +} + +.grey-text { + color: #888 !important; +} + +input.grey-text, +textarea.grey-text { + color: #888 !important; +} + +@media platform == "OSX" { + div.eye-area > input { + font-size: 1em; + } +} \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/src/ui/index.html b/shelled/rustdesk-as-ref/src/ui/index.html new file mode 100644 index 0000000..88c1722 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/shelled/rustdesk-as-ref/src/ui/index.tis b/shelled/rustdesk-as-ref/src/ui/index.tis new file mode 100644 index 0000000..be82652 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/index.tis @@ -0,0 +1,1680 @@ +if (is_osx) view.windowBlurbehind = #light; +stdout.println("current platform:", OS); +stdout.println("is_xfce: ", is_xfce); + +// See default height in common.tis `msgbox()`. +const msgbox_default_height = 180; +const incoming_only_width = 180; + +const outgoing_only = handler.is_outgoing_only(); +const incoming_only = handler.is_incoming_only(); +const disable_installation = handler.is_disable_installation(); +const disable_account = handler.is_disable_account(); +const disable_settings = handler.is_disable_settings(); +const is_custom_client = handler.is_custom_client(); +const disable_ab = handler.is_disable_ab(); +const hide_server_settings = handler.get_builtin_option("hide-server-settings") == "Y"; +const hide_proxy_settings = handler.get_builtin_option("hide-proxy-settings") == "Y"; +const hide_websocket_settings = handler.get_builtin_option("hide-websocket-settings") == "Y"; +const hide_stop_service = handler.get_builtin_option("hide-stop-service") == "Y"; +const disable_change_permanent_password = handler.get_builtin_option("disable-change-permanent-password") == "Y"; +const disable_change_id = handler.get_builtin_option("disable-change-id") == "Y"; + +// html min-width, min-height not working on mac, below works for all +if (incoming_only) { + view.windowMinSize = (scaleIt(incoming_only_width), scaleIt((handler.is_installed() || disable_installation) ? 300 : 390)); +} else { + view.windowMinSize = (scaleIt(560), scaleIt(300)); +} + +var app; +var tmp = handler.get_connect_status(); +var connect_status = tmp[0]; +var service_stopped = handler.get_option("stop-service") == "Y"; +var disable_udp = handler.get_option("disable-udp") == "Y"; +var using_public_server = handler.using_public_server(); +var software_update_url = ""; +var key_confirmed = tmp[1]; +var system_error = ""; + +const default_option_lang = is_custom_client ? 'default' : ''; +const default_option_yes = is_custom_client ? 'Y' : ''; +const default_option_no = is_custom_client ? 'N' : ''; +const default_option_whitelist = is_custom_client ? ',' : ''; +const default_option_approve_mode = is_custom_client ? 'password-click' : ''; + +const grey_text_style = "color:#888;"; + +var svg_menu = + + + +; +var svg_refresh_password = ; + +var my_id = handler.get_id(); +function get_id() { + my_id = handler.get_id(); + return my_id; +} + +function get_msgbox_width(width=500) { + if (incoming_only) { + var maxw = scaleIt(incoming_only_width); + if (width > maxw) width = maxw; + } + return width; +} + +class ConnectStatus: Reactor.Component { + function render() { + return +
    + + {this.getConnectStatusStr()} + {service_stopped ? {translate('Start service')} : ""} +
    ; + } + + function getConnectStatusStr() { + if (service_stopped) { + return translate("Service is not running"); + } else if (connect_status == -1) { + return translate('not_ready_status'); + } else if (connect_status == 0) { + return translate('connecting_status'); + } + if (!handler.using_public_server()) return translate('Ready'); + return {translate("Ready")}, {translate("setup_server_tip")}; + } + + event click $(#start-service) () { + handler.set_option("stop-service", ""); + } + + event click $(#setup-server) () { + handler.open_url("https://rustdesk.com/blog/id-relay-set/"); + } +} + +function createNewConnect(id, type) { + id = id.replace(/\s/g, ""); + app.remote_id.value = formatId(id); + if (!id) return; + var old_id = id; + id = handler.handle_relay_id(id); + var force_relay = old_id != id; + if (id == my_id) { + msgbox("custom-error", "Error", "You cannot connect to your own computer"); + return; + } + handler.set_remote_id(id); + handler.new_remote(id, type, force_relay); +} + +class ShareRdp: Reactor.Component { + function render() { + var rdp_shared_string = translate("Enable RDP session sharing"); + var cls = handler.is_share_rdp() ? "selected" : "line-through"; + return
  • {svg_checkmark}{rdp_shared_string}
  • ; + } + + function onClick() { + handler.set_share_rdp(!handler.is_share_rdp()); + this.update(); + } +} + +var direct_server; +class DirectServer: Reactor.Component { + function this() { + direct_server = this; + } + + function render() { + var text = translate("Enable direct IP access"); + var enabled = handler.get_option("direct-server") == "Y"; + var cls = enabled ? "selected" : "line-through"; + return
  • {svg_checkmark}{text}{enabled && }
  • ; + } + + function onClick() { + if (is_edit_rdp_port) { + is_edit_rdp_port = false; + return; + } + handler.set_option("direct-server", handler.get_option("direct-server") == "Y" ? default_option_no : "Y"); + this.update(); + } +} + +var myIdMenu; +var audioInputMenu; +class AudioInputs: Reactor.Component { + function this() { + audioInputMenu = this; + } + + function render() { + if (!this.show) return
  • ; + var inputs = handler.get_sound_inputs(); + if (is_win) inputs = ["System Sound"].concat(inputs); + if (!inputs.length) return
  • ; + var me = this; + self.timer(1ms, function() { me.toggleMenuState() }); + return
  • {translate('Audio Input')} + +
  • {svg_checkmark}{translate("Mute")}
  • +
    + {inputs.map(function(name) { + return
  • {svg_checkmark}{translate(name)}
  • ; + })} +
    +
  • ; + } + + function get_default() { + if (is_win) return "System Sound"; + return ""; + } + + function get_value() { + return handler.get_option("audio-input") || this.get_default(); + } + + function toggleMenuState() { + var el = this.$(li#enable-audio); + var enabled = handler.get_option(el.id) != "N"; + el.attributes.toggleClass("selected", !enabled); + var is_opt_fixed = handler.is_option_fixed("enable-audio"); + if (disable_settings || is_opt_fixed) { + el.state.disabled = true; + } + var v = this.get_value(); + for (var el in this.$$(menu#audio-input>li)) { + if (el.id == 'enable-audio') continue; + var selected = el.id == v; + el.attributes.toggleClass("selected", selected); + } + } + + event click $(menu#audio-input>li) (_, me) { + if (me.state.disabled) return; + var v = me.id; + if (v == 'enable-audio') { + handler.set_option(v, handler.get_option(v) != 'N' ? 'N' : default_option_yes); + } else { + if (v == this.get_value()) return; + if (v == this.get_default()) v = ""; + handler.set_option("audio-input", v); + } + this.toggleMenuState(); + } +}; + +class Languages: Reactor.Component { + function render() { + var langs = JSON.parse(handler.get_langs()); + var me = this; + self.timer(1ms, function() { me.toggleMenuState() }); + return
  • {translate('Language')} + +
  • {svg_checkmark}Default
  • +
    + {langs.map(function(lang) { + return
  • {svg_checkmark}{lang[1]}
  • ; + })} +
    +
  • ; + } + + + function toggleMenuState() { + var cur = handler.get_local_option("lang") || "default"; + var is_opt_fixed = handler.is_option_fixed("lang"); + for (var el in this.$$(menu#languages>li)) { + var selected = cur == el.id; + el.attributes.toggleClass("selected", selected); + if (is_opt_fixed) { + el.state.disabled = true; + } + } + } + + event click $(menu#languages>li) (_, me) { + if (me.state.disabled) return; + var v = me.id; + if (v == "default") v = default_option_lang; + handler.set_local_option("lang", v); + app.update(); + this.toggleMenuState(); + } +} + +var enhancementsMenu; +class Enhancements: Reactor.Component { + function this() { + enhancementsMenu = this; + } + + function render() { + var has_hwcodec = handler.has_hwcodec(); + var has_vram = handler.has_vram(); + var support_remove_wallpaper = handler.support_remove_wallpaper(); + var me = this; + self.timer(1ms, function() { me.toggleMenuState() }); + return
  • {translate('Enhancements')} + + {(has_hwcodec || has_vram) ?
  • {svg_checkmark}{translate("Enable hardware codec")}
  • : ""} +
  • {svg_checkmark}{translate("Adaptive bitrate")} (beta)
  • +
  • {translate("Recording")}
  • + {support_remove_wallpaper ?
  • {svg_checkmark}{translate("Remove wallpaper during incoming sessions")}
  • : ""} +
  • {svg_checkmark}{translate("keep-awake-during-incoming-sessions-label")}
  • +
    +
  • ; + } + + function toggleMenuState() { + for (var el in $$(menu#enhancements-menu>li)) { + if (el.id && el.id.indexOf("enable-") == 0) { + var enabled = handler.get_option(el.id) != "N"; + el.attributes.toggleClass("selected", enabled); + var is_opt_fixed = handler.is_option_fixed(el.id); + if (is_opt_fixed) { + el.state.disabled = true; + } + } else if (el.id && el.id.indexOf("allow-") == 0) { + var enabled = handler.get_option(el.id) == "Y"; + el.attributes.toggleClass("selected", enabled); + var is_opt_fixed = handler.is_option_fixed(el.id); + if (is_opt_fixed) { + el.state.disabled = true; + } + } else if (el.id == "keep-awake-during-incoming-sessions") { + var enabled = handler.get_option(el.id) != "N"; + el.attributes.toggleClass("selected", enabled); + var is_opt_fixed = handler.is_option_fixed(el.id); + if (is_opt_fixed) { + el.state.disabled = true; + } + } + } + + } + + event click $(menu#enhancements-menu>li) (_, me) { + if (me.state.disabled) return; + var v = me.id; + if (v.indexOf("enable-") == 0) { + var set_value = handler.get_option(v) != 'N' ? 'N' : default_option_yes; + handler.set_option(v, set_value); + if (v == "enable-hwcodec" && set_value != 'N') { + handler.check_hwcodec(); + } + } else if (v.indexOf("allow-") == 0) { + handler.set_option(v, handler.get_option(v) == 'Y' ? default_option_no : 'Y'); + } else if (v == 'keep-awake-during-incoming-sessions') { + handler.set_option(v, handler.get_option(v) != 'N' ? 'N' : default_option_yes); + } else if (v == 'screen-recording') { + var show_root_dir = is_win && handler.is_installed(); + var user_dir = handler.video_save_directory(false); + var root_dir = show_root_dir ? handler.video_save_directory(true) : ""; + var ts0 = handler.get_option("enable-record-session") != 'N' ? { checked: true } : {}; + var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; + var ts2 = handler.get_local_option("allow-auto-record-outgoing") == 'Y' ? { checked: true } : {}; + var is_opt_fixed_enable_record = handler.is_option_fixed("enable-record-session"); + var is_opt_fixed_auto_incoming = handler.is_option_fixed("allow-auto-record-incoming"); + var is_opt_fixed_auto_outgoing = handler.is_option_fixed("allow-auto-record-outgoing"); + var is_opt_fixed_video_dir = handler.is_option_fixed("video-save-directory"); + if (is_opt_fixed_enable_record) { ts0.disabled = true; ts0.style = grey_text_style; } + if (is_opt_fixed_auto_incoming) { ts1.disabled = true; ts1.style = grey_text_style; } + if (is_opt_fixed_auto_outgoing) { ts2.disabled = true; ts2.style = grey_text_style; } + msgbox("custom-recording", translate('Recording'), +
    +
    {translate('Enable recording session')}
    +
    {translate('Automatically record incoming sessions')}
    +
    {translate('Automatically record outgoing sessions')}
    +
    + {show_root_dir ?
    {translate("Incoming")}:  {root_dir}
    : ""} +
    {translate(show_root_dir ? "Outgoing" : "Directory")}:  {user_dir}
    + {is_opt_fixed_video_dir ? "" :
    } +
    +
    + , "", function(res=null) { + if (!res) return; + if (!is_opt_fixed_enable_record) handler.set_option("enable-record-session", res.enable_record_session ? default_option_yes : 'N'); + if (!is_opt_fixed_auto_incoming) handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : default_option_no); + if (!is_opt_fixed_auto_outgoing) handler.set_local_option("allow-auto-record-outgoing", res.auto_record_outgoing ? 'Y' : default_option_no); + if (!is_opt_fixed_video_dir) handler.set_local_option("video-save-directory", $(#folderPath).text); + }, msgbox_default_height, get_msgbox_width()); + } + this.toggleMenuState(); + } +} + +function getUserName() { + try { + return JSON.parse(handler.get_local_option("user_info")).name; + } catch(e) {} + return ''; +} + +function getAccountLabelWithHandle() { + try { + var user = JSON.parse(handler.get_local_option("user_info")); + var username = (user.name || '').trim(); + if (!username) { + return ''; + } + var displayName = (user.display_name || '').trim(); + if (!displayName || displayName == username) { + return username; + } + return displayName + " (@" + username + ")"; + } catch(e) {} + return ''; +} + +// Shared dialog functions +function open_custom_server_dialog() { + var configOptions = handler.get_options(); + var old_relay = configOptions["relay-server"] || ""; + var old_api = configOptions["api-server"] || ""; + var old_id = configOptions["custom-rendezvous-server"] || ""; + var old_key = configOptions["key"] || ""; + msgbox("custom-server", "ID/Relay Server", "
    \ +
    " + translate("ID Server") + ":
    \ +
    " + translate("Relay Server") + ":
    \ +
    " + translate("API Server") + ":
    \ +
    " + translate("Key") + ":
    \ +
    \ + ", "", function(res=null, show_progress) { + if (!res) return; + if (typeof show_progress === 'function') show_progress(); + var id = (res.id || "").trim(); + var relay = (res.relay || "").trim(); + var api = (res.api || "").trim().toLowerCase(); + var key = (res.key || "").trim(); + if (id == old_id && relay == old_relay && key == old_key && api == old_api) return; + if (id) { + var err = handler.test_if_valid_server(id, true); + if (err) { if (typeof show_progress === 'function') show_progress(false, translate("ID Server") + ": " + err); return; } + } + if (relay) { + var err = handler.test_if_valid_server(relay, true); + if (err) { if (typeof show_progress === 'function') show_progress(false, translate("Relay Server") + ": " + err); return; } + } + if (api) { + if (0 != api.indexOf("https://") && 0 != api.indexOf("http://")) { + if (typeof show_progress === 'function') show_progress(false, translate("API Server") + ": " + translate("invalid_http")); + return; + } + } + configOptions["custom-rendezvous-server"] = id; + configOptions["relay-server"] = relay; + configOptions["api-server"] = api; + configOptions["key"] = key; + handler.set_options(configOptions); + if (typeof show_progress === 'function') show_progress(-1); + }, 260, get_msgbox_width()); +} + +function open_whitelist_dialog() { + var is_opt_fixed = handler.is_option_fixed("whitelist"); + var v = handler.get_option("whitelist"); + var old_value = v == default_option_whitelist ? '' : v.split(",").join("\n"); + var type_str = is_opt_fixed ? "custom-whitelist-nook" : "custom-whitelist"; + var readonly_attr = is_opt_fixed ? " readonly=\"readonly\"" : ""; + var grey_class = is_opt_fixed ? " class=\"grey-text\"" : ""; + msgbox(type_str, translate("IP Whitelisting"), "
    \ + " + translate("whitelist_sep") + "
    \ + \ +
    \ + ", "", function(res=null, show_progress) { + if (!res) return; + if (typeof show_progress === 'function') show_progress(); + var value = (res.text || "").trim(); + if (value) { + var values = value.split(/[\s,;\n]+/g); + for (var ip in values) { + if (!ip.match(/^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$/) + && !ip.match(/^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$/)) { + if (typeof show_progress === 'function') show_progress(false, translate("Invalid IP") + ": " + ip); + return; + } + } + value = values.join("\n"); + } + if (value == old_value) return; + if (!value) value = default_option_whitelist; + handler.set_option("whitelist", value.replace("\n", ",")); + if (typeof show_progress === 'function') show_progress(-1); + }, 300, get_msgbox_width()); +} + +function open_proxy_dialog() { + var is_opt_fixed = handler.is_option_fixed("proxy-url"); + var socks5 = handler.get_socks() || {}; + var old_proxy = socks5[0] || ""; + var old_username = socks5[1] || ""; + var old_password = socks5[2] || ""; + var type_str = is_opt_fixed ? "custom-server-nook" : "custom-server"; + var greyStyle = is_opt_fixed ? grey_text_style : ""; + msgbox(type_str, "Socks5/Http(s) Proxy",
    +
    {translate("Server")}:
    +
    {translate("Username")}:
    +
    {translate("Password")}:{ is_opt_fixed ? : }
    +
    + , "", function(res=null, show_progress) { + if (!res) return; + if (typeof show_progress === 'function') show_progress(); + var proxy = (res.proxy || "").trim(); + var username = (res.username || "").trim(); + var password = (res.password || "").trim(); + if (proxy == old_proxy && username == old_username && password == old_password) return; + if (proxy) { + var domain_port = proxy; + var protocol_index = domain_port.indexOf('://'); + if (protocol_index !== -1) { + domain_port = domain_port.substring(protocol_index + 3); + } + var err = handler.test_if_valid_server(domain_port, false); + if (err) { if (typeof show_progress === 'function') show_progress(false, translate("Server") + ": " + err); return; } + } + handler.set_socks(proxy, username, password); + if (typeof show_progress === 'function') show_progress(-1); + }, 240, get_msgbox_width()); +} + +function updateTheme() { + var root_element = self; + if (handler.get_option("allow-darktheme") == "Y") { + // enable dark theme + root_element.attributes.toggleClass("darktheme", true); + } else { + // disable dark theme + root_element.attributes.toggleClass("darktheme", false); + } +} + +class MyIdMenu: Reactor.Component { + function this() { + myIdMenu = this; + } + + function render() { + return
    + {this.renderPop()} + ID{svg_menu} +
    ; + } + + function renderPop() { + var accountLabel = handler.get_local_option("access_token") ? getAccountLabelWithHandle() : ''; + return + + {!disable_settings &&
  • {svg_checkmark}{translate('Enable keyboard/mouse')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable clipboard')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable file transfer')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable camera')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable terminal')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable remote restart')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable TCP tunneling')}
  • } + {!disable_settings && is_win ?
  • {svg_checkmark}{translate('Enable blocking user input')}
  • : ""} + {!disable_settings &&
  • {svg_checkmark}{translate('Enable LAN discovery')}
  • } + + + {!disable_settings &&
  • {svg_checkmark}{translate('Enable remote configuration modification')}
  • } + {!disable_settings &&
    } + {!disable_settings && !hide_server_settings &&
  • {translate('ID/Relay Server')}
  • } + {!disable_settings &&
  • {translate('IP Whitelisting')}
  • } + {!disable_settings && !hide_proxy_settings &&
  • {translate('Socks5/Http(s) Proxy')}
  • } + {!disable_settings && !hide_websocket_settings &&
  • {svg_checkmark}{translate('Use WebSocket')}
  • } + {!disable_settings && !using_public_server && !outgoing_only &&
  • {svg_checkmark}{translate('Disable UDP')}
  • } + {!disable_settings && !using_public_server &&
  • {svg_checkmark}{translate('Allow insecure TLS fallback')}
  • } +
    + {(!hide_stop_service || service_stopped) &&
  • {svg_checkmark}{translate("Enable service")}
  • } + {!disable_settings && is_win && handler.is_installed() ? : ""} + {!disable_settings && } + {!disable_settings && false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } + {!disable_change_id && handler.is_ok_change_id() ?
    : ""} + {!disable_account && (accountLabel ? +
  • {translate('Logout')} ({accountLabel})
  • : +
  • {translate('Login')}
  • )} + {!disable_change_id && !disable_settings && handler.is_ok_change_id() && key_confirmed && connect_status > 0 ?
  • {translate('Change ID')}
  • : ""} +
    +
  • {svg_checkmark}{translate('Dark Theme')}
  • + + {disable_installation ? "" :
  • {svg_checkmark}{translate('Auto update')}
  • } +
  • {translate('About')} {" "}{handler.get_app_name()}
  • + + ; + } + + event click $(svg#menu) (_, me) { + this.showSettingMenu(); + } + + function showSettingMenu() { + audioInputMenu.update({ show: true }); + this.toggleMenuState(); + if (direct_server) direct_server.update(); + var menu = this.$(menu#config-options); + this.$(svg#menu).popup(menu); + } + + event click $(li#login) () { + login(); + } + + event click $(li#logout) () { + logout(); + } + + function toggleMenuState() { + for (var el in $$(menu#config-options>li)) { + var id = el.id; + if (!id) continue; + var is_opt_fixed = handler.is_option_fixed(id); + if (id.indexOf("enable-") == 0) { + var enabled = handler.get_option(id) != "N"; + el.attributes.toggleClass("selected", enabled); + el.attributes.toggleClass("line-through", !enabled); + } else if (id.indexOf("allow-") == 0) { + var enabled = handler.get_option(id) == "Y"; + el.attributes.toggleClass("selected", enabled); + el.attributes.toggleClass("line-through", !enabled); + } else if (id == "whitelist") { + // whitelist should be clickable even when fixed (to view the content) + // The dialog will show readonly textarea and no OK button when fixed + continue; + } + if (is_opt_fixed) { + el.state.disabled = true; + } + } + } + + function showAbout() { + var name = handler.get_app_name(); + msgbox("custom-nocancel-nook-hasclose", translate("About") + " " + name, "
    \ +
    Version: " + handler.get_version() + " \ +
    Fingerprint: " + handler.get_fingerprint() + " \ +
    " + translate("Privacy Statement") + "
    \ +
    " + translate("Website") + "
    \ +
    Copyright © 2025 Purslane Ltd.\ +
    " + handler.get_license() + " \ +

    " + translate("Slogan_tip") + "

    \ +
    \ +
    ", "", function(el) { + if (el && el.attributes) { + handler.open_url(el.attributes['url']); + }; + }, 400, get_msgbox_width()); + } + + event click $(menu#config-options>li) (_, me) { + if (me.state.disabled) return; + if (me.id && me.id.indexOf("enable-") == 0) { + handler.set_option(me.id, handler.get_option(me.id) == "N" ? default_option_yes : "N"); + } + if (me.id && me.id.indexOf("allow-") == 0) { + handler.set_option(me.id, handler.get_option(me.id) == "Y" ? default_option_no : "Y"); + } + if (me.id == "whitelist") { + open_whitelist_dialog(); + } else if (me.id == "custom-server") { + open_custom_server_dialog(); + } else if (me.id == "socks5-server") { + open_proxy_dialog(); + } else if (me.id == "disable-udp") { + handler.set_option("disable-udp", handler.get_option("disable-udp") == "Y" ? "N" : "Y"); + } else if (me.id == "stop-service") { + handler.set_option("stop-service", service_stopped ? default_option_no : "Y"); + } else if (me.id == "change-id") { + var id_label_width = incoming_only ? "50px" : "100px"; + var input_width = incoming_only ? (incoming_only_width - 20) + "px" : "250px"; + msgbox("custom-id", translate("Change ID"), "
    \ +
    " + translate('id_change_tip') + "
    \ +
    ID:
    \ +
    \ + ", "", function(res=null, show_progress) { + if (!res) return; + show_progress(); + var id = (res.id || "").trim(); + if (!id) return; + if (id == my_id) return; + handler.change_id(id); + function check_status() { + var status = handler.get_async_job_status(); + if (status == " ") self.timer(0.1s, check_status); + else { + if (status) show_progress(false, translate(status)); + else show_progress(-1); + } + } + check_status(); + return " "; + }, msgbox_default_height, get_msgbox_width()); + } else if (me.id == "allow-darktheme") { + updateTheme(); + } else if (me.id == "about") { + this.showAbout() + } + } +} + +var is_edit_direct_access_port; +class EditDirectAccessPort: Reactor.Component { + function render() { + return {svg_edit}; + } + + function onMouse(evt) { + if (evt.type == Event.MOUSE_DOWN) { + is_edit_direct_access_port = true; + editDirectAccessPort(); + } + } +} + +function editDirectAccessPort() { + var is_opt_fixed = handler.is_option_fixed("direct-access-port"); + var p0 = handler.get_option('direct-access-port'); + var greyStyle = is_opt_fixed ? grey_text_style : ""; + var port = p0 ? : + ; + var type_str = is_opt_fixed ? "custom-direct-access-port-nook" : "custom-direct-access-port"; + msgbox(type_str, translate('Direct IP Access Settings'),
    +
    {translate('Port')}:{port}
    +
    , "", function(res=null) { + if (!res) return; + var p = (res.port || '').trim(); + if (p) { + p = p.toInteger(); + if (!(p > 0)) { + return translate("Invalid port"); + } + p = p + ''; + } + if (p != p0) handler.set_option('direct-access-port', p); + }, msgbox_default_height, get_msgbox_width()); +} + +class App: Reactor.Component +{ + function this() { + app = this; + } + + function render() { + var is_can_screen_recording = handler.is_can_screen_recording(false); + return +
    +
    +
    + {is_custom_client && handler.get_builtin_option("hide-powered-by-me") != "Y" ?
    {translate('powered_by_me')}
    : ""} +
    + {translate('Your Desktop')} + {outgoing_only ? {svg_menu} : ""} +
    +
    {outgoing_only ? translate('outgoing_only_desk_tip') : translate('desk_tip')}
    + {outgoing_only ?
    : ""} + {!outgoing_only &&
    + + {key_confirmed ? : translate("Generating ...")} +
    } + {!outgoing_only && } +
    + {(!is_win || handler.is_installed() || disable_installation) ? "" : } + {software_update_url && !disable_installation ? : ""} + {is_win && handler.is_installed() && !software_update_url && handler.is_installed_lower_version() && !disable_installation ? : ""} + {is_can_screen_recording ? "": } + {is_can_screen_recording && !handler.is_process_trusted(false) ? : ""} + {!service_stopped && is_can_screen_recording && handler.is_process_trusted(false) && handler.is_installed() && !handler.is_installed_daemon(false) ? : ""} + {system_error ? : ""} + {!system_error && handler.is_login_wayland() && !handler.current_is_wayland() ? : ""} + {!system_error && handler.current_is_wayland() ? : ""} + {incoming_only ? : ""} +
    + {!incoming_only &&
    +
    +
    +
    {translate('Control Remote Desktop')}
    + +
    + + +
    +
    + +
    + {!outgoing_only ? : ""} +
    } +
    +
    ; + } + + event click $(button#connect) { + this.newRemote("connect"); + } + + event click $(button#file-transfer) { + this.newRemote("file-transfer"); + } + + function newRemote(type) { + createNewConnect(this.remote_id.value, type); + } +} + +class InstallMe: Reactor.Component { + function render() { + return
    + +
    {translate('install_tip')}
    +
    +
    ; + } + + event click $(#install-me) { + handler.goto_install(); + } +} + +function download(from, to, args..) { + var rqp = { type:#get, url: from, toFile: to }; + var fn = 0; + var on = 0; + for( var p in args ) { + if( p instanceof Function ) { + switch(++fn) { + case 1: rqp.success = p; break; + case 2: rqp.error = p; break; + case 3: rqp.progress = p; break; + } + } else if( p instanceof Object ) { + switch(++on) { + case 1: rqp.params = p; break; + case 2: rqp.headers = p; break; + } + } + } + view.request(rqp); +} + +// current running version is higher than installed +class UpgradeMe: Reactor.Component { + function render() { + var update_or_download = is_osx ? "download" : "update"; + return
    +
    {translate('Status')}
    +
    {translate('Your installation is lower version.')}
    +
    {translate('Click to upgrade')}
    +
    ; + } + + event click $(#install-me) { + handler.update_me(""); + } +} + +class UpdateMe: Reactor.Component { + function render() { + var update_or_download = "download"; // !is_win ? "download" : "update"; + return
    +
    {translate('Status')}
    +
    There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.
    + {is_custom_client + ?
    {translate('Enable \"Auto update\" or contact your administrator for the latest version.')}
    + :
    {translate('Click to ' + update_or_download)}
    } +
    +
    ; + } + + event click $(#install-me) { + handler.open_url("https://rustdesk.com/download"); + return; + if (!is_win) { + handler.open_url("https://rustdesk.com"); + return; + } + var url = software_update_url + '.' + handler.get_software_ext(); + var path = handler.get_software_store_path(); + var onsuccess = function(md5) { + $(#download-percent).content(translate("Installing ...")); + handler.update_me(path); + }; + var onerror = function(err) { + msgbox("custom-error", "Download Error", "Failed to download"); + }; + var onprogress = function(loaded, total) { + if (!total) total = 5 * 1024 * 1024; + var el = $(#download-percent); + el.style.set{display: "block"}; + el.content("Downloading %" + (loaded * 100 / total)); + }; + stdout.println("Downloading " + url + " to " + path); + download( + url, + self.url(path), + onsuccess, onerror, onprogress); + } +} + +class SystemError: Reactor.Component { + function render() { + return
    +
    {system_error}
    +
    ; + } +} + +class TrustMe: Reactor.Component { + function render() { + return
    +
    {translate('Permissions')}
    +
    {translate('config_acc')}
    +
    {translate('Configure')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#trust-me) { + handler.is_process_trusted(true); + watch_trust(); + } + + event click $(#help-me) { + handler.open_url(translate("doc_mac_permission")); + } +} + +class CanScreenRecording: Reactor.Component { + function render() { + return
    +
    {translate('Permissions')}
    +
    {translate('config_screen')}
    +
    {translate('Configure')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#screen-recording) { + handler.is_can_screen_recording(true); + watch_screen_recording(); + } + + event click $(#help-me) { + handler.open_url(translate("doc_mac_permission")); + } +} + +class InstallDaemon: Reactor.Component { + function render() { + return
    + +
    {translate('install_daemon_tip')}
    +
    {translate('Install')}
    +
    ; + } + + event click $(#install-me) { + handler.is_installed_daemon(true); + } +} + +class FixWayland: Reactor.Component { + function render() { + return
    +
    {translate('Warning')}
    +
    {translate('Login screen using Wayland is not supported')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#help-me) { + handler.open_url(translate("doc_fix_wayland")); + } +} + +class ModifyDefaultLogin: Reactor.Component { + function render() { + return
    +
    {translate('Warning')}
    +
    {translate('wayland_experiment_tip')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#help-me) { + handler.open_url(translate("doc_fix_wayland")); + } +} + +function watch_trust() { + // not use TrustMe::update, because it is buggy + var trusted = handler.is_process_trusted(false); + var el = $(div#trust-me-box); + if (el) { + el.style.set { + display: trusted ? "none" : "block", + }; + } + if (trusted) { + app.update(); + return; + } + self.timer(1s, watch_trust); +} + +function watch_screen_recording() { + var trusted = handler.is_can_screen_recording(false); + var el = $(div#screen-recording-box); + if (el) { + el.style.set { + display: trusted ? "none" : "block", + }; + } + if (trusted) { + app.update(); + return; + } + self.timer(1s, watch_screen_recording); +} + +class PasswordEyeArea : Reactor.Component { + render() { + var method = handler.get_option('verification-method'); + var mode= handler.get_option('approve-mode'); + var hide_one_time = mode == 'click' || method == 'use-permanent-password'; + var value = hide_one_time ? "-" : password_cache[0]; + return +
    + + {hide_one_time ? "" : svg_refresh_password} +
    ; + } + + event click $(svg#refresh-password) (_, me) { + handler.update_temporary_password(); + this.update(); + } +} + +var temporaryPasswordLengthMenu; +class TemporaryPasswordLengthMenu: Reactor.Component { + function this() { + temporaryPasswordLengthMenu = this; + } + + function render() { + if (!this.show) return
  • ; + var me = this; + var method = handler.get_option('verification-method'); + self.timer(1ms, function() { me.toggleMenuState() }); + return
  • {translate("One-time password length")} + +
  • {svg_checkmark}6
  • +
  • {svg_checkmark}8
  • +
  • {svg_checkmark}10
  • +
    +
  • ; + } + + function toggleMenuState() { + var is_opt_fixed = handler.is_option_fixed('temporary-password-length'); + var length = handler.get_option("temporary-password-length"); + var index = ['6', '8', '10'].indexOf(length); + if (index < 0) index = 0; + for (var (i, el) in this.$$(menu#temporary-password-length>li)) { + el.attributes.toggleClass("selected", i == index); + if (is_opt_fixed) { + el.state.disabled = true; + } + } + } + + event click $(menu#temporary-password-length>li) (_, me) { + if (me.state.disabled) return; + var length = me.id.substring('temporary-password-length-'.length); + var old_length = handler.get_option('temporary-password-length'); + if (length != old_length) { + handler.set_option('temporary-password-length', length); + handler.update_temporary_password(); + this.toggleMenuState(); + passwordArea.update(); + } + } +} + +var passwordArea; +class PasswordArea: Reactor.Component { + function this() { + passwordArea = this; + } + + function render() { + var me = this; + self.timer(1ms, function() { me.toggleMenuState() }); + return +
    +
    {translate('One-time Password')}
    +
    + {this.renderPop()} + + {!disable_settings && svg_edit} +
    +
    ; + } + + function renderPop() { + var method = handler.get_option('verification-method'); + var approve_mode= handler.get_option('approve-mode'); + var show_password = approve_mode != 'click'; + var has_local_password = handler.is_local_permanent_password_set(); + return +
  • {svg_checkmark}{translate('Accept sessions via password')}
  • +
  • {svg_checkmark}{translate('Accept sessions via click')}
  • +
  • {svg_checkmark}{translate('Accept sessions via both')}
  • + { !show_password ? '' :
    } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use one-time password')}
  • } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use permanent password')}
  • } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use both passwords')}
  • } + { !show_password ? '' :
    } + { !show_password || disable_change_permanent_password ? '' :
  • {translate('Set permanent password')}
  • } + { !show_password || disable_change_permanent_password ? '' :
  • {translate('Clear permanent password')}
  • } + { !show_password ? '' : } +
    +
  • {svg_checkmark}{translate('enable-2fa-title')}
  • + ; + } + + function toggleMenuState() { + var mode= handler.get_option('approve-mode'); + var mode_id; + if (mode == 'password') + mode_id = 'approve-mode-password'; + else if (mode == 'click') + mode_id = 'approve-mode-click'; + else + mode_id = 'approve-mode-both'; + var pwd_id = handler.get_option('verification-method'); + if (pwd_id != 'use-temporary-password' && pwd_id != 'use-permanent-password') + pwd_id = 'use-both-passwords'; + var has_valid_2fa = handler.has_valid_2fa(); + for (var el in this.$$(menu#edit-password-context>li)) { + if (el.id.indexOf("approve-mode-") == 0) { + el.attributes.toggleClass("selected", el.id == mode_id); + if (handler.is_option_fixed('approve-mode')) { + el.state.disabled = true; + } + } + if (el.id.indexOf("use-") == 0) { + el.attributes.toggleClass("selected", el.id == pwd_id); + if (handler.is_option_fixed('verification-method')) { + el.state.disabled = true; + } + } + if (el.id == "clear-password") { + var has_local_password = handler.is_local_permanent_password_set(); + el.state.disabled = !has_local_password; + } + if (el.id == "tfa") + el.attributes.toggleClass("selected", has_valid_2fa); + } + } + + event click $(svg#edit) (_, me) { + var approve_mode= handler.get_option('approve-mode'); + var show_password = approve_mode != 'click'; + if(show_password && temporaryPasswordLengthMenu) temporaryPasswordLengthMenu.update({show: true }); + var menu = $(menu#edit-password-context); + me.popup(menu); + } + + event click $(li#set-password) { + var me = this; + var has_local_password = handler.is_local_permanent_password_set(); + var permanent_password_set = handler.is_permanent_password_set(); + var password_hidden_tip = translate('password-hidden-tip'); + var preset_password_tip = translate('preset-password-in-use-tip'); + var password_tip = ""; + if (has_local_password) { + password_tip = "
    [!] " + password_hidden_tip + "
    "; + } else if (permanent_password_set) { + password_tip = "
    [!] " + preset_password_tip + "
    "; + } + msgbox("custom-password", translate("Set Password"), "
    \ +
    " + translate('Password') + ":
    \ +
    " + translate('Confirmation') + ":
    \ + " + password_tip + " \ +
    \ + ", "", function(res=null) { + if (!res) return; + var p0 = (res.password || "").trim(); + var p1 = (res.confirmation || "").trim(); + if (p0.length == 0 && p1.length == 0) { + return " "; + } + if (p0.length < 6 && p0.length != 0) { + return translate("Too short, at least 6 characters."); + } + if (p0 != p1) { + return translate("The confirmation is not identical."); + } + handler.set_permanent_password(p0); + me.update(); + }, msgbox_default_height, get_msgbox_width()); + self.timer(30ms, function() { + updateSetPasswordSubmitState(); + }); + } + + event click $(li#clear-password) { + if (this.$(li#clear-password).state.disabled) return; + handler.set_permanent_password(""); + this.update(); + } + + event click $(menu#edit-password-context>li) (_, me) { + if (me.state.disabled) return; + if (me.id.indexOf('use-') == 0) { + handler.set_option('verification-method', me.id); + this.toggleMenuState(); + passwordArea.update(); + } else if (me.id.indexOf('approve-mode') == 0) { + var approve_mode; + if (me.id == 'approve-mode-password') + approve_mode = 'password'; + else if (me.id == 'approve-mode-click') + approve_mode = 'click'; + else + approve_mode = default_option_approve_mode; + handler.set_option('approve-mode', approve_mode); + this.toggleMenuState(); + passwordArea.update(); + } + } + + event click $(li#tfa) { + var me = this; + var has_valid_2fa = handler.has_valid_2fa(); + if (has_valid_2fa) { + handler.set_option('2fa', ''); + me.update(); + } else { + var new2fa = handler.generate2fa(); + var src = handler.generate_2fa_img_src(new2fa); + msgbox("custom-2fa-setting", translate('enable-2fa-title'), +
    +
    {translate('enable-2fa-desc')}
    + +
    +
    + , "", function(res=null) { + if (!res) return; + if (!res.code) return; + if (!handler.verify2fa(res.code)) { + return translate('wrong-2fa-code'); + } + me.update(); + }, 400, get_msgbox_width()); + } + } +} + +var password_cache = ["","","",""]; +function updatePasswordArea() { + self.timer(1s, function() { + var temporary_password = handler.temporary_password(); + var verification_method = handler.get_option('verification-method'); + var temporary_password_length = handler.get_option('temporary-password-length'); + var approve_mode = handler.get_option('approve-mode'); + var update = false; + if (password_cache[0] != temporary_password) { + password_cache[0] = temporary_password; + update = true; + } + if (password_cache[1] != verification_method) { + password_cache[1] = verification_method; + update = true; + } + if (password_cache[2] != temporary_password_length) { + password_cache[2] = temporary_password_length; + update = true; + } + if (password_cache[3] != approve_mode) { + password_cache[3] = approve_mode; + update = true; + } + if (update && passwordArea) passwordArea.update(); + updatePasswordArea(); + }); +} +if (!outgoing_only) updatePasswordArea(); + +function updateSetPasswordSubmitState() { + var dialog = $(#msgbox); + if (!dialog) return; + var password = dialog.$(input[name='password']); + var confirmation = dialog.$(input[name='confirmation']); + var submit = dialog.$(button#submit); + if (!password || !confirmation || !submit) return; + var can_submit = (password.value || "").trim().length > 0 || + (confirmation.value || "").trim().length > 0; + submit.state.disabled = !can_submit; +} + +class ID: Reactor.Component { + function render() { + return ; + } + + // https://github.com/c-smile/sciter-sdk/blob/master/doc/content/sciter/Event.htm + event change { + var fid = formatId(this.value); + var d = this.value.length - (this.old_value || "").length; + this.old_value = this.value; + var start = this.xcall(#selectionStart) || 0; + var end = this.xcall(#selectionEnd); + if (fid == this.value || d <= 0 || start != end) { + return; + } + // fix Caret position + this.value = fid; + var text_after_caret = this.old_value.substr(start); + var n = fid.length - formatId(text_after_caret).length; + this.xcall(#setSelection, n, n); + } +} + +var reg = /^\d+$/; +function formatId(id) { + id = id.replace(/\s/g, ""); + if (reg.test(id) && id.length > 3) { + var n = id.length; + var a = n % 3 || 3; + var new_id = id.substr(0, a); + for (var i = a; i < n; i += 3) { + new_id += " " + id.substr(i, 3); + } + return new_id; + } + return id; +} + +event keydown (evt) { + if (view.focus && view.focus.id != 'remote_id') { + return; + } + if (!evt.shortcutKey) { + if (isEnterKey(evt)) { + var el = $(button#connect); + view.focus = el; + el.sendEvent("click"); + // simulate button click effect, windows does not have this issue + el.attributes.toggleClass("active", true); + self.timer(0.3s, function() { + el.attributes.toggleClass("active", false); + }); + } + } +} + +event keyup $(#msgbox input[name='password']) { + updateSetPasswordSubmitState(); +} + +event keyup $(#msgbox input[name='confirmation']) { + updateSetPasswordSubmitState(); +} + +event change $(#msgbox input[name='password']) { + updateSetPasswordSubmitState(); +} + +event change $(#msgbox input[name='confirmation']) { + updateSetPasswordSubmitState(); +} + +$(body).content(
    ); + +event click $(#powered-by) { + handler.open_url("https://rustdesk.com"); +} + +event click $(#open-settings) (_, me) { + showSettings(); +} + +// Event handlers for outgoing_only mode (when menu items are in main UI, not in MyIdMenu) +event click $(li#custom-server) (_, me) { + if (!outgoing_only) return; + open_custom_server_dialog(); +} + +event click $(li#whitelist) (_, me) { + if (!outgoing_only) return; + open_whitelist_dialog(); +} + +event click $(li#socks5-server) (_, me) { + if (!outgoing_only) return; + open_proxy_dialog(); +} + +event click $(li#login) (_, me) { + if (!outgoing_only) return; + login(); +} + +function self.closing() { + var (x, y, w, h) = view.box(#rectw, #border, #screen); + handler.closing(x, y, w, h); + return true; +} + +function self.ready() { + var r = handler.get_size(); + if (isReasonableSize(r) && r[2] > 0) { + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + if (r[2] >= sw && r[3] >= sh) { + self.timer(1ms, function() { view.windowState = View.WINDOW_MAXIMIZED; }); + } else { + view.move(r[0], r[1], incoming_only ? scaleIt(incoming_only_width) : r[2], r[3]); + } + } else { + centerize(scaleIt(incoming_only ? incoming_only_width : 800), scaleIt(incoming_only ? 390 : 600)); + } + if (!handler.get_remote_id()) { + view.focus = $(#remote_id); + } + refreshCurrentUser(); + updateTheme(); +} + +function showAbout() { + myIdMenu.showAbout(); +} + +function showSettings() { + if ($(#overlay).style#display == 'block') return; + var menu = myIdMenu.$(menu#config-options); + var anchor = $(#open-settings); + if (!anchor) anchor = myIdMenu.$(svg#menu); + // show immediately at button, then update menu state asynchronously + anchor.popup(menu); + self.timer(1ms, function() { + audioInputMenu.update({ show: true }); + myIdMenu.toggleMenuState(); + if (direct_server) direct_server.update(); + }); +} + +function checkConnectStatus() { + handler.check_mouse_time(); // trigger connection status updater + self.timer(1s, function() { + var tmp = handler.get_option("stop-service") == "Y"; + if (tmp != service_stopped) { + service_stopped = tmp; + app.update(); + } + tmp = handler.using_public_server(); + if (tmp != using_public_server) { + using_public_server = tmp; + app.connect_status.update(); + } + tmp = handler.get_connect_status(); + if (tmp[0] != connect_status) { + connect_status = tmp[0]; + app.connect_status.update(); + myIdMenu.update(); + } + if (tmp[1] != key_confirmed) { + key_confirmed = tmp[1]; + app.update(); + } + if (tmp[2] && tmp[2] != my_id) { + stdout.println("id updated"); + app.update(); + } + tmp = handler.get_error(); + if (system_error != tmp) { + system_error = tmp; + app.update(); + } + tmp = handler.get_software_update_url(); + if (tmp != software_update_url) { + software_update_url = tmp; + app.update(); + } + if (handler.recent_sessions_updated()) { + stdout.println("recent sessions updated"); + updateAbPeer(); + app.update(); + } + tmp = handler.get_option("disable-udp") == "Y"; + if (tmp != disable_udp) { + disable_udp = tmp; + app.update(); + } + check_if_overlay(); + checkConnectStatus(); + }); +} + +var enter = false; +function self.onMouse(evt) { + switch(evt.type) { + case Event.MOUSE_ENTER: + enter = true; + check_if_overlay(); + break; + case Event.MOUSE_LEAVE: + $(#overlay).style#display = 'none'; + enter = false; + break; + } +} + +function check_if_overlay() { + var enabled; + var is_enabled_by_control_permissions = handler.is_remote_modify_enabled_by_control_permissions(); + if (is_enabled_by_control_permissions == "true") { + enabled = true; + } else if (is_enabled_by_control_permissions == "false") { + enabled = false; + } else { + enabled = handler.get_option('allow-remote-config-modification') == 'Y'; + } + if (!enabled) { + var time0 = getTime(); + handler.check_mouse_time(); + self.timer(120ms, function() { + if (!enter) return; + var d = time0 - handler.get_mouse_time(); + if (d < 120) $(#overlay).style#display = 'block'; + }); + } +} + +checkConnectStatus(); + +function set_local_user_info(user) { + var user_info = {name: user.name}; + if (user.display_name) { + user_info.display_name = user.display_name; + } + if (user.avatar) { + user_info.avatar = user.avatar; + } + if (user.status) { + user_info.status = user.status; + } + handler.set_local_option("user_info", JSON.stringify(user_info)); +} + +function login() { + var name0 = getUserName(); + var pass0 = ''; + msgbox("custom-login", translate('Login'),
    +
    {translate('Username')}:
    +
    {translate('Password')}:
    +
    , "", function(res=null, show_progress) { + if (!res) return; + show_progress(); + var name = (res.username || '').trim(); + if (!name) { + show_progress(false, translate("Username missed")); + return " "; + } + var pass = (res.password || '').trim(); + if (!pass) { + show_progress(false, translate("Password missed")); + return " "; + } + abLoading = true; + var url = handler.get_api_server(); + httpRequest(url + "/api/login", #post, {username: name, password: pass, id: my_id, uuid: handler.get_uuid(), type: 'account', deviceInfo: getDeviceInfo()}, function(data) { + if (data.error) { + abLoading = false; + var err = translate(data.error); + show_progress(false, err); + return; + } + if (data.type == 'email_check') { + abLoading = false; + show_progress(-1); + on_2fa_check(data); + return; + } + handler.set_local_option("access_token", data.access_token); + set_local_user_info(data.user); + show_progress(-1); + myIdMenu.update(); + getAb(); + }, function(err, status) { + abLoading = false; + err = translate(err); + if (url.indexOf('rustdesk') < 0) err = url + ', ' + err; + show_progress(false, err); + }); + return " "; + }, msgbox_default_height, get_msgbox_width()); +} + +function on_2fa_check(last_msg) { + const isEmailCheck = !last_msg.tfa_type || last_msg.tfa_type == 'email_check'; + const secret = last_msg.secret; + const emailHint = last_msg.user.email; + + msgbox("custom-2fa-verification-code", translate('Verification code'),
    + { isEmailCheck &&
    {translate('Email')}:{emailHint}
    } +
    {translate(isEmailCheck ? 'Verification code' : '2FA code')}:
    + { isEmailCheck &&
    {translate('verification_tip')}
    } +
    , "", + function(res=null, show_progress) { + if (!res) return; + show_progress(); + var code = (res.verification_code || '').trim(); + if (!code || code.length < 6) { + show_progress(false, translate("Too short, at least 6 characters.")); + return " "; + } + abLoading = true; + var url = handler.get_api_server(); + const loginData = { + username: last_msg.user.name, + id: my_id, + uuid: handler.get_uuid(), + type: 'email_code', + verificationCode: code, + tfaCode: isEmailCheck ? '' : code, + secret: secret, + deviceInfo: getDeviceInfo() + }; + httpRequest(url + "/api/login", #post, loginData, + function(data) { + if (data.error) { + abLoading = false; + show_progress(false, data.error); + return; + } + handler.set_local_option("access_token", data.access_token); + set_local_user_info(data.user); + show_progress(-1); + myIdMenu.update(); + getAb(); + }, + function(err, status) { + abLoading = false; + err = translate(err); + if (url.indexOf('rustdesk') < 0) err = url + ', ' + err; + show_progress(false, err); + } + ); + return " "; + }, + msgbox_default_height, + get_msgbox_width() + ); +} + +function reset_token() { + handler.set_local_option("access_token", ""); + handler.set_local_option("user_info", ""); + handler.set_local_option("selected-tags", ""); + myIdMenu.update(); + resetAb(); + if (abComponent) { + abComponent.update(); + } +} + +function logout() { + var url = handler.get_api_server(); + httpRequest(url + "/api/logout", #post, {id: my_id, uuid: handler.get_uuid()}, function(data) { + }, function(err, status) { + msgbox("custom-error", translate('Error'), err); + }, getHttpHeaders()); + reset_token(); +} + +function refreshCurrentUser() { + var token = handler.get_local_option("access_token"); + if (!token) { return; } + abLoading = true; + abError = ""; + app.update(); + httpRequest(handler.get_api_server() + "/api/currentUser", #post, {id: my_id, uuid: handler.get_uuid()}, function(data) { + if (data.error) { + if (data.error == 'Invalid token') { + reset_token(); + } + handleAbError(data.error); + return; + } + if (!handler.verify_login(data.verifier, token)) { + handleAbError("Please update your self-hosting server Pro to latest version"); + return; + } + set_local_user_info(data); + myIdMenu.update(); + getAb(); + }, function(err, status) { + if (status == 401 || status == 400) { + reset_token(); + } + handleAbError(err); + }, getHttpHeaders()); +} + +function getHttpHeaders() { + return "Authorization: Bearer " + handler.get_local_option("access_token"); +} + +function getDeviceInfo() { + return JSON.parse(handler.get_login_device_info()); +} diff --git a/shelled/rustdesk-as-ref/src/ui/install.html b/shelled/rustdesk-as-ref/src/ui/install.html new file mode 100644 index 0000000..bd9653e --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/install.html @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/shelled/rustdesk-as-ref/src/ui/install.tis b/shelled/rustdesk-as-ref/src/ui/install.tis new file mode 100644 index 0000000..fad4071 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/install.tis @@ -0,0 +1,70 @@ +function self.ready() { + centerize(scaleIt(800), scaleIt(600)); +} + +var install_path = ""; + +class Install: Reactor.Component { + function render() { + const install_options = JSON.parse(view.install_options()); + const desktop_icon = { checked: install_options?.DESKTOPSHORTCUTS == '0' ? false : true }; + const startmenu_shortcuts = { checked: install_options?.STARTMENUSHORTCUTS == '0' ? false : true }; + return
    +
    {translate('Installation')}
    +
    {translate('Installation Path')} {": "} + +
    +
    {translate('Create start menu shortcuts')}
    +
    {translate('Create desktop icon')}
    +
    {translate('End-user license agreement')}
    +
    {translate('agreement_tip')}
    +
    +
    + + + + {handler.show_run_without_install() && } +
    +
    ; + } + + event click $(#cancel) { + view.close(); + } + + event click $(#run-without-install) { + handler.run_without_install(); + } + + event click $(#path) { + install_path = view.selectFolder() || ""; + if (install_path) { + install_path = install_path.urlUnescape(); + install_path = install_path.replace("file://", "").replace("/", "\\"); + if (install_path[install_path.length - 1] != "\\") install_path += "\\"; + install_path += handler.get_app_name(); + $(#path_input).value = install_path; + } + } + + event click $(#agreement) { + view.open_url("http://rustdesk.com/privacy"); + } + + event click $(#submit) { + for (var el in $$(button)) el.state.disabled = true; + $(progress).style.set{ display: "inline-block" }; + var args = ""; + if ($(#startmenu).value) { + args += "startmenu "; + } + if ($(#desktopicon).value) { + args += "desktopicon "; + } + view.install_me(args, install_path); + } +} + +$(body).content(); diff --git a/shelled/rustdesk-as-ref/src/ui/msgbox.tis b/shelled/rustdesk-as-ref/src/ui/msgbox.tis new file mode 100644 index 0000000..6e6b6a6 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/msgbox.tis @@ -0,0 +1,390 @@ +function translate_text(text) { + if (text.indexOf('Failed') == 0 && text.indexOf(': ') > 0) { + var fds = text.split(': '); + for (var i = 0; i < fds.length; ++i) { + fds[i] = translate(fds[i]); + } + text = fds.join(': '); + } else { + var fds = text.split(' '); + if (fds.length > 1 && fds[0].slice(-4) === '_tip') { + fds[0] = translate(fds[0]); + var rest = text.substring(fds[0].length + 1); + text = fds[0] + ' ' + translate(rest); + } else { + text = translate(text); + } + } + return text; +} + +var msgboxTimerFunc = function() {} +function closeMsgbox() { + self.timer(0, msgboxTimerFunc); + $(#msgbox).content(); +} + +class MsgboxComponent: Reactor.Component { + function this(params) { + this.width = params.width; + this.height = params.height; + this.type = params.type; + this.title = params.title; + this.content = params.content; + this.link = params.link; + this.remember = params.remember; + this.callback = params.callback; + this.hasRetry = params.hasRetry; + this.autoLogin = params.autoLogin; + this.contentStyle = params.contentStyle; + try { this.content = translate_text(this.content); } catch (e) {} + } + + function getIcon(color) { + if (this.type == "input-password" || this.type == "session-login" || this.type == "session-login-password" || this.type == "input-2fa") { + return ; + } + if (this.type == "connecting") { + return ; + } + if (this.type == "success") { + return ; + } + if (this.type.indexOf("error") >= 0 || this.type == "re-input-password" || this.type == "input-2fa" || this.type == "session-re-login" || this.type == "session-login-re-password") { + return ; + } + return null; + } + + function getInputPasswordContent() { + var ts = this.remember ? { checked: true } : {}; + return
    +
    {translate('Please enter your password')}
    + +
    {translate('Remember password')}
    +
    ; + } + + function get2faContent() { + var enable_trusted_devices = handler.get_enable_trusted_devices(); + return
    +
    {translate('enter-2fa-title')}
    +
    + {enable_trusted_devices ?
    {translate('Trust this device')}
    : ""} +
    ; + } + + function getInputUserPasswordContent() { + return
    +
    {translate("OS Username")}
    +
    +
    {translate("OS Password")}
    + +
    +
    ; + } + + function getXsessionPasswordContent() { + return
    +
    {translate("OS Username")}
    +
    +
    {translate("OS Password")}
    + +
    {translate('Please enter your password')}
    + +
    {translate('Remember password')}
    +
    ; + } + + function getContent() { + if (this.type == "input-password") { + return this.getInputPasswordContent(); + } else if (this.type == "input-2fa") { + return this.get2faContent(); + } else if (this.type == "session-login") { + return this.getInputUserPasswordContent(); + } else if (this.type == "session-login-password") { + return this.getXsessionPasswordContent(); + } else if (this.type == "custom-os-password") { + var ts = this.autoLogin ? { checked: true } : {}; + return
    + +
    {translate('Auto Login')}
    +
    ; + } + return this.content; + } + + function getColor() { + if (this.type == "input-password" || this.type == "input-2fa" || this.type == "custom-os-password" || this.type == "session-login" || this.type == "session-login-password") { + return "#AD448E"; + } + if (this.type == "success") { + return "#32bea6"; + } + if (this.type.indexOf("error") >= 0 || this.type == "re-input-password" || this.type == "session-re-login" || this.type == "session-login-re-password") { + return "#e04f5f"; + } + return "#2C8CFF"; + } + + function hasSkip() { + return this.type.indexOf("skip") >= 0; + } + + function getScreenshotButtons() { + var isScreenshot = this.type.indexOf("take-screenshot") >= 0; + return isScreenshot + ?
    + + + +
    + : ""; + } + + function render() { + this.set_outline_focus(); + var color = this.getColor(); + var icon = this.getIcon(color); + var content = this.getContent(); + var hasCancel = this.type.indexOf("error") < 0 && this.type.indexOf("nocancel") < 0 && this.type != "restarting"; + var hasOk = this.type != "connecting" && this.type != "success" && this.type.indexOf("nook") < 0; + var hasLink = this.link != ""; + var hasClose = this.type.indexOf("hasclose") >= 0; + var show_progress = this.type == "connecting"; + var me = this; + self.timer(0, msgboxTimerFunc); + msgboxTimerFunc = function() { + if (typeof content == "string") + me.$(#content).html = translate(content); + else + me.$(#content).content(content); + }; + self.timer(3ms, msgboxTimerFunc); + return (
    +
    +
    +
    + {translate(this.title)} +
    +
    +
    + {icon &&
    {icon}
    } +
    +
    +
    + + + {hasCancel || this.hasRetry ? : ""} + {this.hasSkip() ? : ""} + {hasOk || this.hasRetry ? : ""} + {hasLink ? : ""} + {hasClose ? : ""} + {this.getScreenshotButtons()} +
    +
    +
    +
    ); + } + + event click $(.custom-event) (_, me) { + if (this.callback) this.callback(me); + } + + function submit() { + var submit_btn = this.$(button#submit); + if (submit_btn) { + if (submit_btn.state.disabled) return; + submit_btn.sendEvent("click"); + } + } + + function cancel() { + if (this.$(button#cancel)) { + this.$(button#cancel).sendEvent("click"); + } + } + + event click $(button#cancel) { + this.close(); + if (this.callback) this.callback(null); + } + + event click $(button#skip) { + var values = this.getValues(); + values.skip = true; + if (this.callback) this.callback(values); + if (this.close) this.close(); + } + + event click $(button#jumplink) { + if (this.link.indexOf("http") == 0) { + Sciter.launch(this.link); + } + } + + event click $(button#submit) { + if (this.type == "error") { + if (this.hasRetry) { + retryConnect(true); + return; + } + } + if (this.type == "re-input-password") { + this.type = "input-password"; + this.update(); + return; + } + if (this.type == "session-re-login") { + this.type = "session-login"; + this.update(); + return; + } + if (this.type == "session-login-re-password") { + this.type = "session-login-password"; + this.update(); + return; + } + var values = this.getValues(); + if (this.callback) { + var self = this; + var err = this.callback(values, function(a=1, b='') { self.show_progress(a, b); }); + if (!err) { + if (this.close) this.close(); + return; + } + if (err && err.trim()) this.show_progress(false, err); + } else { + this.close(); + } + } + + event click $(button#screenshotSaveAs) { + this.close(); + + handler.leave(handler.get_keyboard_mode()); + const filter = "Png file (*.png)"; + const defaultExt = "png"; + const initialPath = System.path(#USER_DOCUMENTS, "screenshot"); + const caption = "Save as"; + var url = view.selectFile(#save, filter, defaultExt, initialPath, caption); + handler.enter(handler.get_keyboard_mode()); + if(url) { + var res = handler.handle_screenshot("0:" + URL.toPath(url)); + if (res) { + msgbox("custom-error-nocancel-nook-hasclose", "Take screenshot", res, "", function() {}); + } + } else { + handler.handle_screenshot("2"); + } + } + + event click $(button#screenshotCopyToClip) { + this.close(); + var res = handler.handle_screenshot("1"); + if (res) { + msgbox("custom-error-nocancel-nook-hasclose", "Take screenshot", res, "", function() {}); + } + } + + event click $(button#screenshotCancel) { + this.close(); + handler.handle_screenshot("2"); + } + + event keydown (evt) { + if (!evt.shortcutKey) { + if (isEnterKey(evt)) { + this.submit(); + } + if (evt.keyCode == Event.VK_ESCAPE) { + this.cancel(); + } + } + } + + event click $(button#select_directory) { + var folder = view.selectFolder(translate("Change"), $(#folderPath).text); + if (folder) { + if (folder.indexOf("file://") == 0) folder = folder.substring(7); + $(#folderPath).text = folder; + } + } + + function show_progress(show=1, err="") { + if (show == -1) { + this.close() + return; + } + this.$(#progress).style.set { + display: show ? "inline-block" : "none" + }; + this.$(#error).text = err; + } + + function getValues() { + var values = { type: this.type }; + for (var el in this.$$(.form input)) { + values[el.attributes["name"]] = el.value; + } + for (var el in this.$$(.form textarea)) { + values[el.attributes["name"]] = el.value; + } + for (var el in this.$$(.form button)) { + values[el.attributes["name"]] = el.value; + } + if (this.type == "input-password") { + values.password = (values.password || "").trim(); + if (!values.password) { + return; + } + } + if (this.type == "input-2fa") { + values.code = (values.code || "").trim(); + if (!values.code) { + return; + } + } + if (this.type == "session-login") { + values.osusername = (values.osusername || "").trim(); + values.ospassword = (values.ospassword || "").trim(); + if (!values.osusername || !values.ospassword) { + return; + } + } + if (this.type == "session-login-password") { + values.password = (values.password || "").trim(); + values.osusername = (values.osusername || "").trim(); + values.ospassword = (values.ospassword || "").trim(); + if (!values.osusername || !values.ospassword || !values.password) { + return; + } + } + if (this.type == "multiple-sessions-nocancel") { + values.sid = (this.$$(select))[0].value; + } + if (this.type == "remote-printer-selector") { + values.name = (this.$$(select))[0].value; + } + return values; + } + + function set_outline_focus() { + var me = this; + self.timer(30ms, function() { + var el = me.$(.outline-focus); + if (el) view.focus = el; + else { + el = me.$(#submit); + if (el) { + view.focus = el; + } + } + }); + } + + function close() { + closeMsgbox(); + } +} diff --git a/shelled/rustdesk-as-ref/src/ui/port_forward.tis b/shelled/rustdesk-as-ref/src/ui/port_forward.tis new file mode 100644 index 0000000..a30f698 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/port_forward.tis @@ -0,0 +1,77 @@ +class PortForward: Reactor.Component { + function render() { + var args = handler.get_args(); + var is_rdp = handler.is_rdp(); + if (is_rdp) { + this.pfs = [["", "", "RDP"]]; + args = ["rdp"]; + } else if (args.length) { + this.pfs = [args]; + } else { + this.pfs = handler.get_port_forwards(); + } + var pfs = this.pfs.map(function(pf, i) { + return + {is_rdp ? : pf[0]} + {args.length ? svg_arrow : ""} + {pf[1] || "localhost"} + {pf[2]} + {args.length ? "" : {svg_cancel}} + ; + }); + return
    + {pfs.length ?
    + {translate('Listening ...')}
    + {translate('not_close_tcp_tip')} +
    : ""} + + + + + + + {args.length ? "" : } + + + + {args.length ? "" : + + + + + + + + } + {pfs} + +
    {translate('Local Port')} + {translate('Remote Host')}{translate('Remote Port')}{translate('Action')}
    {svg_arrow}
    ; + } + + event click $(#add) () { + var port = ($(#port).value || "").toInteger() || 0; + var remote_host = $(#remote-host).value || ""; + var remote_port = ($(#remote-port).value || "").toInteger() || 0; + if (port <= 0 || remote_port <= 0) return; + handler.add_port_forward(port, remote_host, remote_port); + this.update(); + } + + event click $(#new-rdp) { + handler.new_rdp(); + } + + event click $(.remove svg) (_, me) { + var pf = this.pfs[me.parent.parent.index - 1]; + handler.remove_port_forward(pf[0]); + this.update(); + } +} + +function initializePortForward() +{ + $(#file-transfer-wrapper).content(); + $(#video-wrapper).style.set { visibility: "hidden", position: "absolute" }; + $(#file-transfer-wrapper).style.set { display: "block" }; +} diff --git a/shelled/rustdesk-as-ref/src/ui/printer.tis b/shelled/rustdesk-as-ref/src/ui/printer.tis new file mode 100644 index 0000000..c284826 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/printer.tis @@ -0,0 +1,41 @@ +include "sciter:reactor.tis"; + +handler.printerRequest = function(id, path) { + show_printer_selector(id, path); +}; + +function show_printer_selector(id, path) +{ + var names = handler.get_printer_names(); + msgbox("remote-printer-selector", "Incoming Print Job", , "", function(res=null) { + if (res && res.name) { + handler.on_printer_selected(id, path, res.name); + } + }, 180); +} + +class PrinterComponent extends Reactor.Component { + this var names = []; + this var jobTip = translate("print-incoming-job-confirm-tip"); + + function this(params) { + if (params && params.names) { + this.names = params.names; + } + } + + function render() { + return
    +
    {translate("print-incoming-job-confirm-tip")}
    +
    +
    + +
    +
    +
    ; + } +} diff --git a/shelled/rustdesk-as-ref/src/ui/remote.css b/shelled/rustdesk-as-ref/src/ui/remote.css new file mode 100644 index 0000000..71b2c16 --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/remote.css @@ -0,0 +1,46 @@ +body { + margin: 0; + color: black; + overflow: scroll-indicator; +} + +div#video-wrapper { + size: *; + background: #212121; +} + +div#quality-monitor { + top: 20px; + right: 20px; + background: #7571719c; + padding: 5px; + min-width: 150px; + color: azure; + border: 0.5px solid azure; +} + +video#handler { + behavior: native-remote video; + size: *; + margin: *; + foreground-size: contain; +} + +img#cursor { + position: absolute; + display: none; + //opacity: 0.66, + //transform: scale(0.8); +} + +.goup { + transform: rotate(90deg); +} + +table#remote-folder-view { + context-menu: selector(menu#remote-folder-view); +} + +table#local-folder-view { + context-menu: selector(menu#local-folder-view); +} \ No newline at end of file diff --git a/shelled/rustdesk-as-ref/src/ui/remote.html b/shelled/rustdesk-as-ref/src/ui/remote.html new file mode 100644 index 0000000..70e909d --- /dev/null +++ b/shelled/rustdesk-as-ref/src/ui/remote.html @@ -0,0 +1,44 @@ + + + + + + +
    + + +
    + + + +
    + + +
    +