diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index 1c0fec8d21f9..9d56fbe6f2b5 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -73,7 +73,7 @@ jobs: - name: Install flutter rust bridge deps shell: bash run: | - cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + 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 diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 4f57ca71012f..6c59be972ac7 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -33,8 +33,8 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.07.12 VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" - VERSION: "1.3.3" - NDK_VERSION: "r27b" + VERSION: "1.3.4" + NDK_VERSION: "r27c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" @@ -107,7 +107,6 @@ jobs: # https://github.com/flutter/flutter/issues/155685 - name: Replace engine with rustdesk custom flutter engine - if: false run: | flutter doctor -v flutter precache --windows @@ -738,7 +737,7 @@ jobs: shell: bash run: | cd "$(dirname "$(which flutter)")" - # https://github.com/flutter/flutter/issues/133533 + # https://github.com/flutter/flutter/issues/1.3.43 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 @@ -1065,7 +1064,7 @@ jobs: 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 }} + cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked case ${{ matrix.job.target }} in aarch64-linux-android) ./flutter/ndk_arm64.sh diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 843e07835cdb..282f678e8304 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -18,7 +18,7 @@ env: VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" # vcpkg version: 2024.06.15 VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" - VERSION: "1.3.3" + VERSION: "1.3.4" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" @@ -149,7 +149,7 @@ jobs: 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" + 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 @@ -302,7 +302,7 @@ jobs: - 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" + 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 @@ -347,7 +347,7 @@ jobs: 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 }} + cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked case ${{ matrix.job.target }} in aarch64-linux-android) ./flutter/ndk_arm64.sh diff --git a/Cargo.lock b/Cargo.lock index 3e52f9a96eb6..169615e945a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5219,7 +5219,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/rustdesk-org/rdev#961d25cc00c6b3ef80f444e6a7bed9872e2c35ea" +source = "git+https://github.com/rustdesk-org/rdev#01ac3ec8009f04f7615842b9152338844b806184" dependencies = [ "cocoa 0.24.1", "core-foundation 0.9.4", @@ -5494,7 +5494,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.3" +version = "1.3.4" dependencies = [ "android-wakelock", "android_logger", @@ -5594,7 +5594,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.3" +version = "1.3.4" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index dbf819bf8389..3cc05b780551 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.3" +version = "1.3.4" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index a6bd632dc0cb..65a73ee52b68 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.3 + version: 1.3.4 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 9ea820fec475..430400721a8b 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.3 + version: 1.3.4 exec: usr/lib/rustdesk/rustdesk exec_args: $@ apt: diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 5a5f56b204e0..4920ade6d960 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -135,8 +135,8 @@ docker build -t "rustdesk-builder" . # 构建容器 ``` 在Dockerfile的RUN apt update之前插入两行: - RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list - RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list + 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`后插入下面代码: diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index e0a4eb5991b9..1821c529afb5 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -1,7 +1,5 @@ #!/bin/bash -set -x - # # Script to build F-Droid release of RustDesk # @@ -23,6 +21,43 @@ set -x # + 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}" @@ -82,21 +117,6 @@ export PATH="${PATH}:${HOME}/flutter/bin:${HOME}/depot_tools" export VCPKG_ROOT="${HOME}/vcpkg" -prepare_Flutter() { - version="${1}" - - pushd "${HOME}" - if [ ! -f "${HOME}/flutter/bin/flutter" ]; then - git clone https://github.com/flutter/flutter - fi - pushd flutter - git restore . - git checkout "${version}" - flutter config --no-analytics - popd # flutter - popd # ${HOME} -} - # Now act depending on build step # NOTE: F-Droid maintainers require explicit declaration of dependencies @@ -116,15 +136,20 @@ prebuild) .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 + FLUTTER_BRIDGE_VERSION="$(yq -r \ .env.FLUTTER_VERSION \ .github/workflows/bridge.yml)" @@ -207,14 +232,16 @@ prebuild) cargo install \ cargo-ndk \ - --version "${CARGO_NDK_VERSION}" + --version "${CARGO_NDK_VERSION}" \ + --locked # Install rust bridge generator cargo install cargo-expand cargo install flutter_rust_bridge_codegen \ --version "${FLUTTER_RUST_BRIDGE_VERSION}" \ - --features "uuid" + --features "uuid" \ + --locked # Populate native vcpkg dependencies @@ -277,46 +304,66 @@ prebuild) git apply res/fdroid/patches/*.patch - # Backup .gclient file, for later restore + # 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 - cp flutter-sdk/.gclient flutter-sdk/.gclient.bak + if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then + # Install Flutter bridge version - # For FLUTTER_BRIDGE_VERSION - sed \ - -i \ - -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' \ - flutter/pubspec.yaml + prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter" - # Install Flutter bridge version - prepare_Flutter "${FLUTTER_BRIDGE_VERSION}" - cp flutter-sdk/.gclient.bak flutter-sdk/.gclient - sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_BRIDGE_VERSION}/" flutter-sdk/.gclient + # Save changes - # Download Flutter dependencies - pushd flutter - flutter clean && flutter packages pub get - popd # flutter + 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 - # Generate FFI bindings - flutter_rust_bridge_codegen \ - --rust-input ./src/flutter_ffi.rs \ - --dart-output ./flutter/lib/generated_bridge.dart + # Download Flutter dependencies - git restore flutter/pubspec.* + pushd flutter - # Install Flutter - prepare_Flutter "${FLUTTER_VERSION}" - cp flutter-sdk/.gclient.bak flutter-sdk/.gclient - sed -i "s/FLUTTER_VERSION_PLACEHOLDER/${FLUTTER_VERSION}/" flutter-sdk/.gclient + 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 + + # 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 + fi + + # Install Flutter version for RustDesk library build + + prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter" # gms is not in thoes 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 thoes files now, but we still keep the following lines. + # `firebase_analytics` is not in these files now, but we still keep the following lines. + sed \ -i \ -e '/firebase_analytics/d' \ @@ -343,9 +390,12 @@ build) # '.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 \ @@ -381,7 +431,8 @@ build) pushd flutter - flutter clean && flutter packages pub get + flutter clean + flutter packages pub get popd # flutter diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index fb389b45e646..13ee4dd84a43 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import 'dart:io'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -2730,30 +2731,6 @@ Future osxRequestAudio() async { return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); } -class DraggableNeverScrollableScrollPhysics extends ScrollPhysics { - /// Creates scroll physics that does not let the user scroll. - const DraggableNeverScrollableScrollPhysics({super.parent}); - - @override - DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { - return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor)); - } - - @override - bool shouldAcceptUserOffset(ScrollMetrics position) { - // TODO: find a better solution to check if the offset change is caused by the scrollbar. - // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity]. - if (position is ScrollPositionWithSingleContext) { - // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member - return position.activity is IdleScrollActivity; - } - return false; - } - - @override - bool get allowImplicitScrolling => false; -} - Widget futureBuilder( {required Future? future, required Widget Function(dynamic data) hasData}) { return FutureBuilder( @@ -3483,6 +3460,35 @@ Widget buildPresetPasswordWarning() { ); } +bool get isLinuxMateDesktop => + isLinux && + (Platform.environment['XDG_CURRENT_DESKTOP']?.toLowerCase() == 'mate' || + Platform.environment['XDG_SESSION_DESKTOP']?.toLowerCase() == 'mate' || + Platform.environment['DESKTOP_SESSION']?.toLowerCase() == 'mate'); + +Map? _linuxOsDistro; + +String getLinuxOsDistroId() { + if (_linuxOsDistro == null) { + String osInfo = bind.getOsDistroInfo(); + if (osInfo.isEmpty) { + _linuxOsDistro = {}; + } else { + try { + _linuxOsDistro = jsonDecode(osInfo); + } catch (e) { + debugPrint('Failed to parse os info: $e'); + // Don't call `bind.getOsDistroInfo()` again if failed to parse osInfo. + _linuxOsDistro = {}; + } + } + } + return (_linuxOsDistro?['id'] ?? '') as String; +} + +bool get isLinuxMint => + getLinuxOsDistroId().toLowerCase().contains('linuxmint'); + // https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44 Widget buildVirtualWindowFrame(BuildContext context, Widget child) { boxShadow() => isMainDesktopWindow diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 78bd20ef0336..ae07c1498cf1 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -1,5 +1,6 @@ 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'; @@ -316,13 +317,14 @@ class _AddressBookState extends State { Widget _buildTags() { return Obx(() { - final List tags; + List tags; if (gFFI.abModel.sortTags.value) { tags = gFFI.abModel.currentAbTags.toList(); tags.sort(); } else { - tags = gFFI.abModel.currentAbTags; + tags = gFFI.abModel.currentAbTags.toList(); } + tags = [kUntagged, ...tags].toList(); final editPermission = gFFI.abModel.current.canWrite(); tagBuilder(String e) { return AddressBookTag( @@ -669,6 +671,14 @@ class _AddressBookState extends State { } 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 } @@ -741,12 +751,14 @@ class AddressBookTag extends StatelessWidget { } const double radius = 8; + final isUnTagged = name == kUntagged; + final showAction = showActionMenu && !isUnTagged; return GestureDetector( onTap: onTap, - onTapDown: showActionMenu ? setPosition : null, - onSecondaryTapDown: showActionMenu ? setPosition : null, - onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null, - onLongPress: showActionMenu ? () => _showMenu(context, pos) : null, + 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) @@ -758,17 +770,18 @@ class AddressBookTag extends StatelessWidget { child: IntrinsicWidth( child: Row( children: [ - Container( - width: radius, - height: radius, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: tags.contains(name) - ? Colors.white - : gFFI.abModel.getCurrentAbTagColor(name)), - ).marginOnly(right: radius / 2), + 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(name, + child: Text(isUnTagged ? translate(name) : name, style: TextStyle( overflow: TextOverflow.ellipsis, color: tags.contains(name) ? Colors.white : null)), diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 265b79d0760e..68d82116b6bd 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -5,7 +5,7 @@ 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/desktop/widgets/scroll_wrapper.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'; @@ -270,33 +270,24 @@ class _PeersViewState extends State<_PeersView> }, ) : peerCardUiType.value == PeerUiType.list - ? DesktopScrollWrapper( - scrollController: _scrollController, - child: ListView.builder( - controller: _scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - 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); - }), + ? 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); + }, ) - : DesktopScrollWrapper( - scrollController: _scrollController, - child: DynamicGridView.builder( - controller: _scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithWrapping( - mainAxisSpacing: space / 2, - crossAxisSpacing: space), - itemCount: peers.length, - itemBuilder: (BuildContext context, int index) { - return buildOnePeer(peers[index], false); - }), - )); + : 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(); @@ -532,15 +523,22 @@ class AddressBookPeersView extends BasePeersView { 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 selectedTags) { + for (final tag in selectedNormalTags) { if (!idents.contains(tag)) { return false; } } return true; } else { - for (final tag in selectedTags) { + for (final tag in selectedNormalTags) { if (idents.contains(tag)) { return true; } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index c313958bddfc..f6f9c4d34f9a 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -244,10 +244,6 @@ const double kDesktopIconButtonSplashRadius = 20; /// [kMinCursorSize] indicates min cursor (w, h) const int kMinCursorSize = 12; -/// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse -const kDefaultScrollAmountMultiplier = 5.0; -const kDefaultScrollDuration = Duration(milliseconds: 50); -const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kFullScreenEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0; // Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead. diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 9728d6b478e2..04a186b84c63 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -12,7 +12,6 @@ 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/scroll_wrapper.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/plugin/ui_manager.dart'; @@ -125,47 +124,43 @@ class _DesktopHomePageState extends State child: Container( width: isIncomingOnly ? 280.0 : 200.0, color: Theme.of(context).colorScheme.background, - child: DesktopScrollWrapper( - scrollController: _leftPaneScrollController, - child: Stack( - children: [ - SingleChildScrollView( - controller: _leftPaneScrollController, - physics: DraggableNeverScrollableScrollPhysics(), - child: Column( - key: _childKey, - children: children, - ), + child: Stack( + children: [ + SingleChildScrollView( + controller: _leftPaneScrollController, + child: Column( + key: _childKey, + children: children, ), - 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, - ), + ), + 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, ), + onTap: () => { + if (DesktopSettingPage.tabKeys.isNotEmpty) + { + DesktopSettingPage.switch2page( + DesktopSettingPage.tabKeys[0]) + } + }, + onHover: (value) => _editHover.value = value, ), - ) - ], - ), + ), + ) + ], ), ), ); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 69100470f0ea..577b1e8c5a68 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -19,7 +19,6 @@ import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import '../../common/widgets/dialog.dart'; import '../../common/widgets/login.dart'; @@ -226,13 +225,11 @@ class _DesktopSettingPageState extends State Expanded( child: Container( color: Theme.of(context).scaffoldBackgroundColor, - child: DesktopScrollWrapper( - scrollController: controller, - child: PageView( - controller: controller, - physics: NeverScrollableScrollPhysics(), - children: _children(), - )), + child: PageView( + controller: controller, + physics: NeverScrollableScrollPhysics(), + children: _children(), + ), ), ) ], @@ -281,13 +278,10 @@ class _DesktopSettingPageState extends State Widget _listView({required List<_TabInfo> tabs}) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: tabs.map((tab) => _listItem(tab: tab)).toList(), - )); + return ListView( + controller: scrollController, + children: tabs.map((tab) => _listItem(tab: tab)).toList(), + ); } Widget _listItem({required _TabInfo tab}) { @@ -349,22 +343,19 @@ class _GeneralState extends State<_General> { @override Widget build(BuildContext context) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - physics: DraggableNeverScrollableScrollPhysics(), - 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)); + 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() { @@ -705,29 +696,26 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); - return DesktopScrollWrapper( - scrollController: scrollController, - child: SingleChildScrollView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - child: Column( - children: [ - _lock(locked, 'Unlock Security Settings', () { - locked = false; - setState(() => {}); - }), - AbsorbPointer( - absorbing: locked, - child: Column(children: [ - permissions(context), - password(context), - _Card(title: '2FA', children: [tfa()]), - _Card(title: 'ID', children: [changeId()]), - more(context), - ]), - ), - ], - )).marginOnly(bottom: _kListViewBottomMargin)); + return SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + _lock(locked, 'Unlock Security Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + permissions(context), + password(context), + _Card(title: '2FA', children: [tfa()]), + _Card(title: 'ID', children: [changeId()]), + more(context), + ]), + ), + ], + )).marginOnly(bottom: _kListViewBottomMargin); } Widget tfa() { @@ -1374,57 +1362,62 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { bool get wantKeepAlive => true; bool locked = !isWeb && bind.mainIsInstalled(); + final scrollController = ScrollController(); + late final TextEditingController idController; + late final TextEditingController relayController; + late final TextEditingController apiController; + late final TextEditingController keyController; + + @override + void initState() { + super.initState(); + Map oldOptions = jsonDecode(bind.mainGetOptionsSync()); + old(String key) { + return (oldOptions[key] ?? '').trim(); + } + + idController = TextEditingController(text: old('custom-rendezvous-server')); + relayController = TextEditingController(text: old('relay-server')); + apiController = TextEditingController(text: old('api-server')); + keyController = TextEditingController(text: old('key')); + } + @override Widget build(BuildContext context) { super.build(context); bool enabled = !locked; - final scrollController = ScrollController(); final hideServer = bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; // TODO: support web proxy final hideProxy = isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - controller: scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - children: [ - _lock(locked, 'Unlock Network Settings', () { - locked = false; - setState(() => {}); - }), - AbsorbPointer( - absorbing: locked, - child: Column(children: [ - if (!hideServer) server(enabled), - if (!hideProxy) - _Card(title: 'Proxy', children: [ - _Button('Socks5/Http(s) Proxy', changeSocks5Proxy, - enabled: enabled), - ]), - ]), - ), - ]).marginOnly(bottom: _kListViewBottomMargin)); + return ListView(controller: scrollController, children: [ + _lock(locked, 'Unlock Network Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + if (!hideServer) server(enabled), + if (!hideProxy) + _Card(title: 'Proxy', children: [ + _Button('Socks5/Http(s) Proxy', changeSocks5Proxy, + enabled: enabled), + ]), + ]), + ), + ]).marginOnly(bottom: _kListViewBottomMargin); } server(bool enabled) { // Simple temp wrapper for PR check tmpWrapper() { // Setting page is not modal, oldOptions should only be used when getting options, never when setting. - Map oldOptions = jsonDecode(bind.mainGetOptionsSync()); - old(String key) { - return (oldOptions[key] ?? '').trim(); - } RxString idErrMsg = ''.obs; RxString relayErrMsg = ''.obs; RxString apiErrMsg = ''.obs; - var idController = - TextEditingController(text: old('custom-rendezvous-server')); - var relayController = TextEditingController(text: old('relay-server')); - var apiController = TextEditingController(text: old('api-server')); - var keyController = TextEditingController(text: old('key')); final controllers = [ idController, relayController, @@ -1494,19 +1487,14 @@ class _DisplayState extends State<_Display> { @override Widget build(BuildContext context) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - controller: scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - children: [ - viewStyle(context), - scrollStyle(context), - imageQuality(context), - codec(context), - if (!isWeb) privacyModeImpl(context), - other(context), - ]).marginOnly(bottom: _kListViewBottomMargin)); + return ListView(controller: scrollController, children: [ + viewStyle(context), + scrollStyle(context), + imageQuality(context), + codec(context), + if (!isWeb) privacyModeImpl(context), + other(context), + ]).marginOnly(bottom: _kListViewBottomMargin); } Widget viewStyle(BuildContext context) { @@ -1729,15 +1717,12 @@ class _AccountState extends State<_Account> { @override Widget build(BuildContext context) { final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: [ - _Card(title: 'Account', children: [accountAction(), useInfo()]), - ], - ).marginOnly(bottom: _kListViewBottomMargin)); + return ListView( + controller: scrollController, + children: [ + _Card(title: 'Account', children: [accountAction(), useInfo()]), + ], + ).marginOnly(bottom: _kListViewBottomMargin); } Widget accountAction() { @@ -1834,18 +1819,14 @@ class _PluginState extends State<_Plugin> { Widget build(BuildContext context) { bind.pluginListReload(); final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: ChangeNotifierProvider.value( - value: pluginManager, - child: Consumer(builder: (context, model, child) { - return ListView( - physics: DraggableNeverScrollableScrollPhysics(), - controller: scrollController, - children: model.plugins.map((entry) => pluginCard(entry)).toList(), - ).marginOnly(bottom: _kListViewBottomMargin); - }), - ), + 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); + }), ); } @@ -1897,75 +1878,72 @@ class _AboutState extends State<_About> { final fingerprint = data['fingerprint'].toString(); const linkStyle = TextStyle(decoration: TextDecoration.underline); final scrollController = ScrollController(); - return DesktopScrollWrapper( - scrollController: scrollController, - child: SingleChildScrollView( - controller: scrollController, - physics: DraggableNeverScrollableScrollPhysics(), - 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), - ) - ], + 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), ), - ), - ], - )), - ).marginSymmetric(vertical: 4.0) - ], - ).marginOnly(left: _kContentHMargin) - ]), - )); + Text( + translate('Slogan_tip'), + style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white), + ) + ], + ), + ), + ], + )), + ).marginSymmetric(vertical: 4.0) + ], + ).marginOnly(left: _kContentHMargin) + ]), + ); }); } } @@ -2283,26 +2261,39 @@ _LabeledTextField( String errorText, bool enabled, bool secure) { - return Row( + return Table( + columnWidths: const { + 0: FixedColumnWidth(150), + 1: FlexColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 140), - child: Text( - '${translate(label)}:', - textAlign: TextAlign.right, - style: TextStyle( - fontSize: 16, color: disabledTextColor(context, enabled)), - ).marginOnly(right: 10)), - Expanded( - child: TextField( + 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), + errorText: errorText.isNotEmpty ? errorText : null, + ), style: TextStyle( color: disabledTextColor(context, enabled), - )), + ), + ), + ], ), ], ).marginOnly(bottom: 8); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index cca2074a242c..912b06b0282b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; import 'package:flutter_hbb/models/state_model.dart'; import '../../consts.dart'; @@ -742,12 +741,6 @@ class _ImagePaintState extends State { ScrollController horizontal, ScrollController vertical, ) { - final scrollConfig = CustomMouseWheelScrollConfig( - scrollDuration: kDefaultScrollDuration, - scrollCurve: Curves.linearToEaseOut, - mouseWheelTurnsThrottleTimeMs: - kDefaultMouseWheelThrottleDuration.inMilliseconds, - scrollAmountMultiplier: kDefaultScrollAmountMultiplier); var widget = child; if (layoutSize.width < size.width) { widget = ScrollConfiguration( @@ -793,36 +786,26 @@ class _ImagePaintState extends State { ); } if (layoutSize.width < size.width) { - widget = ImprovedScrolling( - scrollController: horizontal, - enableCustomMouseWheelScrolling: cursorOverImage.isFalse, - customMouseWheelScrollConfig: scrollConfig, - child: RawScrollbar( - thickness: kScrollbarThickness, - thumbColor: Colors.grey, - controller: horizontal, - thumbVisibility: false, - trackVisibility: false, - notificationPredicate: layoutSize.height < size.height - ? (notification) => notification.depth == 1 - : defaultScrollNotificationPredicate, - child: widget, - ), + 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 = ImprovedScrolling( - scrollController: vertical, - enableCustomMouseWheelScrolling: cursorOverImage.isFalse, - customMouseWheelScrollConfig: scrollConfig, - child: RawScrollbar( - thickness: kScrollbarThickness, - thumbColor: Colors.grey, - controller: vertical, - thumbVisibility: false, - trackVisibility: false, - child: widget, - ), + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: vertical, + thumbVisibility: false, + trackVisibility: false, + child: widget, ); } diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart deleted file mode 100644 index c5bc3394b439..000000000000 --- a/flutter/lib/desktop/widgets/scroll_wrapper.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; - -class DesktopScrollWrapper extends StatelessWidget { - final ScrollController scrollController; - final Widget child; - const DesktopScrollWrapper( - {Key? key, required this.scrollController, required this.child}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return ImprovedScrolling( - scrollController: scrollController, - enableCustomMouseWheelScrolling: true, - // enableKeyboardScrolling: true, // strange behavior on mac - customMouseWheelScrollConfig: CustomMouseWheelScrollConfig( - scrollDuration: kDefaultScrollDuration, - scrollCurve: Curves.linearToEaseOut, - mouseWheelTurnsThrottleTimeMs: - kDefaultMouseWheelThrottleDuration.inMilliseconds, - scrollAmountMultiplier: kDefaultScrollAmountMultiplier), - child: child, - ); - } -} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 00afbb001e76..3176bfb86eba 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -483,7 +483,16 @@ class _AppState extends State with WidgetsBindingObserver { child = keyListenerBuilder(context, child); } if (isLinux) { - child = buildVirtualWindowFrame(context, child); + // `(!(isLinuxMateDesktop || isLinuxMint))` is not used here for clarity. + // `isLinuxMint` will call ffi function. + if (!isLinuxMateDesktop) { + if (!isLinuxMint) { + debugPrint( + 'Linux distro is not linuxmint, and desktop is not mate, ' + 'so we build virtual window frame.'); + child = buildVirtualWindowFrame(context, child); + } + } } return child; }, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 89b71c177c91..de68aa510b67 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -48,6 +48,9 @@ class _ConnectionPageState extends State { bool isPeersLoaded = false; StreamSubscription? _uniLinksSubscription; + // https://github.com/flutter/flutter/issues/157244 + Iterable _autocompleteOpts = []; + _ConnectionPageState() { if (!isWeb) _uniLinksSubscription = listenUniLinks(); _idController.addListener(() { @@ -166,7 +169,7 @@ class _ConnectionPageState extends State { child: Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { - return const Iterable.empty(); + _autocompleteOpts = const Iterable.empty(); } else if (peers.isEmpty && !isPeersLoaded) { Peer emptyPeer = Peer( id: '', @@ -182,7 +185,7 @@ class _ConnectionPageState extends State { rdpUsername: '', loginName: '', ); - return [emptyPeer]; + _autocompleteOpts = [emptyPeer]; } else { String textWithoutSpaces = textEditingValue.text.replaceAll(" ", ""); @@ -194,7 +197,7 @@ class _ConnectionPageState extends State { } String textToFind = textEditingValue.text.toLowerCase(); - return peers + _autocompleteOpts = peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -206,6 +209,7 @@ class _ConnectionPageState extends State { peer.alias.toLowerCase().contains(textToFind)) .toList(); } + return _autocompleteOpts; }, fieldViewBuilder: (BuildContext context, TextEditingController fieldTextEditingController, @@ -274,6 +278,7 @@ class _ConnectionPageState extends State { optionsViewBuilder: (BuildContext context, AutocompleteOnSelected onSelected, Iterable options) { + options = _autocompleteOpts; double maxHeight = options.length * 50; if (options.length == 1) { maxHeight = 52; diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 1dee69b94ee7..fa7c35bb64e2 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -26,6 +26,19 @@ import '../widgets/dialog.dart'; final initText = '1' * 1024; +// Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard. +// When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard. +// https://github.com/flutter/flutter/issues/159384 +// https://github.com/flutter/flutter/issues/159383 +void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { + if (isAndroid) { + if (isKeyboardVisible != true) { + // `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`. + gFFI.invokeMethod("enable_soft_keyboard", false); + } + } +} + class RemotePage extends StatefulWidget { RemotePage({Key? key, required this.id, this.password, this.isSharedPassword}) : super(key: key); @@ -99,6 +112,8 @@ class _RemotePageState extends State with WidgetsBindingObserver { if (gFFI.recordingModel.start) { showToast(translate('Automatically record outgoing sessions')); } + _disableAndroidSoftKeyboard( + isKeyboardVisible: keyboardVisibilityController.isVisible); }); WidgetsBinding.instance.addObserver(this); } @@ -857,6 +872,8 @@ class _KeyHelpToolsState extends State { final pi = gFFI.ffiModel.pi; final isMac = pi.platform == kPeerPlatformMacOS; + final isWin = pi.platform == kPeerPlatformWindows; + final isLinux = pi.platform == kPeerPlatformLinux; final modifiers = [ wrap('Ctrl ', () { setState(() => inputModel.ctrl = !inputModel.ctrl); @@ -937,6 +954,28 @@ class _KeyHelpToolsState extends State { wrap('PgDn', () { inputModel.inputKey('VK_NEXT'); }), + // to-do: support PrtScr on Mac + if (isWin || isLinux) + wrap('PrtScr', () { + inputModel.inputKey('VK_SNAPSHOT'); + }), + if (isWin || isLinux) + wrap('ScrollLock', () { + inputModel.inputKey('VK_SCROLL'); + }), + if (isWin || isLinux) + wrap('Pause', () { + inputModel.inputKey('VK_PAUSE'); + }), + if (isWin || isLinux) + // Maybe it's better to call it "Menu" + // https://en.wikipedia.org/wiki/Menu_key + wrap('Menu', () { + inputModel.inputKey('Apps'); + }), + wrap('Enter', () { + inputModel.inputKey('VK_ENTER'); + }), SizedBox(width: 9999), wrap('', () { inputModel.inputKey('VK_LEFT'); @@ -1244,7 +1283,9 @@ void showOptions( toggles + [privacyModeWidget]), ); - }, clickMaskDismiss: true, backDismiss: true); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); } TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) { @@ -1263,7 +1304,9 @@ TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) { children: children, ), ); - }, clickMaskDismiss: true, backDismiss: true); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); }, ); } @@ -1305,7 +1348,9 @@ TTextMenu? getResolutionMenu(FFI ffi, String id) { children: children, ), ); - }, clickMaskDismiss: true, backDismiss: true); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); }, ); } diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 0da84e0f26c8..3aa722a5abf1 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -33,6 +33,8 @@ bool filterAbTagByIntersection() { const _personalAddressBookName = "My address book"; const _legacyAddressBookName = "Legacy address book"; +const kUntagged = "Untagged"; + enum ForcePullAb { listAndCurrent, current, @@ -424,6 +426,7 @@ class AbModel { // #region tags Future addTags(List tagList) async { + tagList.removeWhere((e) => e == kUntagged); final ret = await current.addTags(tagList, {}); await pullNonLegacyAfterChange(); _saveCache(); @@ -645,6 +648,9 @@ class AbModel { } Color getCurrentAbTagColor(String tag) { + if (tag == kUntagged) { + return MyTheme.accent; + } int? colorValue = current.tagColors[tag]; if (colorValue != null) { return Color(colorValue); diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 0692ba2df4d4..a30bb79fdbd3 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -1080,7 +1080,7 @@ class InputModel { onExit: true, ); - static int tryGetNearestRange(int v, int min, int max, int n) { + static double tryGetNearestRange(double v, double min, double max, double n) { if (v < min && v >= min - n) { v = min; } @@ -1138,8 +1138,8 @@ class InputModel { return; } evtValue = { - 'x': pos.x, - 'y': pos.y, + 'x': pos.x.toInt(), + 'y': pos.y.toInt(), }; } @@ -1221,8 +1221,8 @@ class InputModel { evt['x'] = '0'; evt['y'] = '0'; } else { - evt['x'] = '${pos.x}'; - evt['y'] = '${pos.y}'; + evt['x'] = '${pos.x.toInt()}'; + evt['y'] = '${pos.y.toInt()}'; } Map mapButtons = { @@ -1362,31 +1362,27 @@ class InputModel { y = pos.dy; } - var evtX = 0; - var evtY = 0; - try { - evtX = x.round(); - evtY = y.round(); - } catch (e) { - debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e'); - return null; - } - return InputModel.getPointInRemoteRect( - true, peerPlatform, kind, evtType, evtX, evtY, rect, + true, peerPlatform, kind, evtType, x, y, rect, buttons: buttons); } - static Point? getPointInRemoteRect(bool isLocalDesktop, String? peerPlatform, - String kind, String evtType, int evtX, int evtY, Rect rect, + static Point? getPointInRemoteRect( + bool isLocalDesktop, + String? peerPlatform, + String kind, + String evtType, + double evtX, + double evtY, + Rect rect, {int buttons = kPrimaryMouseButton}) { - int minX = rect.left.toInt(); + double minX = rect.left; // https://github.com/rustdesk/rustdesk/issues/6678 // For Windows, [0,maxX], [0,maxY] should be set to enable window snapping. - int maxX = (rect.left + rect.width).toInt() - + double maxX = (rect.left + rect.width) - (peerPlatform == kPeerPlatformWindows ? 0 : 1); - int minY = rect.top.toInt(); - int maxY = (rect.top + rect.height).toInt() - + double minY = rect.top; + double maxY = (rect.top + rect.height) - (peerPlatform == kPeerPlatformWindows ? 0 : 1); evtX = InputModel.tryGetNearestRange(evtX, minX, maxX, 5); evtY = InputModel.tryGetNearestRange(evtY, minY, maxY, 5); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d029aa3951ff..e5cde939fb76 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1268,7 +1268,9 @@ class ImageModel with ChangeNotifier { rgba, rect?.width.toInt() ?? 0, rect?.height.toInt() ?? 0, - isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, + isWeb | isWindows | isLinux + ? ui.PixelFormat.rgba8888 + : ui.PixelFormat.bgra8888, ); if (parent.target?.id != pid) return; await update(image); @@ -2184,7 +2186,7 @@ class CursorModel with ChangeNotifier { if (dx == 0 && dy == 0) return; - Point? newPos; + Point? newPos; final rect = parent.target?.ffiModel.rect; if (rect == null) { // unreachable @@ -2195,8 +2197,8 @@ class CursorModel with ChangeNotifier { parent.target?.ffiModel.pi.platform, kPointerEventKindMouse, kMouseEventTypeDefault, - (_x + dx).toInt(), - (_y + dy).toInt(), + _x + dx, + _y + dy, rect, buttons: kPrimaryButton); if (newPos == null) { @@ -2204,8 +2206,8 @@ class CursorModel with ChangeNotifier { } dx = newPos.x - _x; dy = newPos.y - _y; - _x = newPos.x.toDouble(); - _y = newPos.y.toDouble(); + _x = newPos.x; + _y = newPos.y; if (tryMoveCanvasX && dx != 0) { parent.target?.canvasModel.panX(-dx * scale); } @@ -2859,6 +2861,7 @@ class FFI { canvasModel.scale, ffiModel.pi.currentDisplay); } + imageModel.callbacksOnFirstImage.clear(); await imageModel.update(null); cursorModel.clear(); ffiModel.clear(); diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 20891281455d..ffbf6638253d 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1828,5 +1828,9 @@ class RustdeskImpl { throw UnimplementedError("sessionGetConnToken"); } + String getOsDistroInfo({dynamic hint}) { + return ''; + } + void dispose() {} } diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 56b85ccae6dd..9fa947002d3e 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -16,6 +16,9 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) extern bool gIsConnectionManager; +GtkWidget *find_gl_area(GtkWidget *widget); +void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view); + // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); @@ -39,9 +42,10 @@ static void my_application_activate(GApplication* application) { // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; + GdkScreen* screen = NULL; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { + screen = gtk_window_get_screen(window); + if (screen != NULL && GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; @@ -76,6 +80,8 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + try_set_transparent(window, screen, view); + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); @@ -121,3 +127,106 @@ MyApplication* my_application_new() { "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } + +GtkWidget *find_gl_area(GtkWidget *widget) +{ + if (GTK_IS_GL_AREA(widget)) { + return widget; + } + + if (GTK_IS_CONTAINER(widget)) { + GList *children = gtk_container_get_children(GTK_CONTAINER(widget)); + for (GList *iter = children; iter != NULL; iter = g_list_next(iter)) { + GtkWidget *child = GTK_WIDGET(iter->data); + GtkWidget *gl_area = find_gl_area(child); + if (gl_area != NULL) { + g_list_free(children); + return gl_area; + } + } + g_list_free(children); + } + + return NULL; +} + +bool is_linux_mint() +{ + bool is_mint = false; + char line[256]; + FILE *fp = fopen("/etc/os-release", "r"); + if (fp == NULL) { + return false; + } + while (fgets(line, sizeof(line), fp)) { + if (strstr(line, "ID=linuxmint") != NULL) { + is_mint = true; + break; + } + } + fclose(fp); + + return is_mint; +} + +bool is_desktop_mate() +{ + const char* desktop = NULL; + desktop = getenv("XDG_CURRENT_DESKTOP"); + printf("Linux desktop, XDG_CURRENT_DESKTOP: %s\n", desktop == NULL ? "" : desktop); + if (desktop == NULL) { + desktop = getenv("XDG_SESSION_DESKTOP"); + printf("Linux desktop, XDG_SESSION_DESKTOP: %s\n", desktop == NULL ? "" : desktop); + } + if (desktop == NULL) { + desktop = getenv("DESKTOP_SESSION"); + printf("Linux desktop, DESKTOP_SESSION: %s\n", desktop == NULL ? "" : desktop); + } + if (desktop != NULL && strcasecmp(desktop, "mate") == 0) { + return true; + } + return false; +} + +bool skip_setting_transparent() +{ + if (is_desktop_mate()) { + printf("Linux desktop, MATE\n"); + return true; + } + + if (is_linux_mint()) { + printf("Linux desktop, Linux Mint\n"); + return true; + } + + return false; +} + +// https://github.com/flutter/flutter/issues/152154 +// Remove this workaround when flutter version is updated. +void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view) +{ + GtkWidget *gl_area = NULL; + + if (skip_setting_transparent()) { + printf("Skip setting transparent\n"); + return; + } + + printf("Try setting transparent\n"); + + gl_area = find_gl_area(GTK_WIDGET(view)); + if (gl_area != NULL) { + gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); + } + + if (screen != NULL) { + GdkVisual *visual = NULL; + gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE); + visual = gdk_screen_get_rgba_visual(screen); + if (visual != NULL && gdk_screen_is_composited(screen)) { + gtk_widget_set_visual(GTK_WIDGET(window), visual); + } + } +} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 7c60e037a57d..58df59a5521f 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -335,7 +335,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "519350f1f40746798299e94786197d058353bac9" + resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033" url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -530,15 +530,6 @@ packages: url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer" source: git version: "0.0.1" - flutter_improved_scrolling: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "62f09545149f320616467c306c8c5f71714a18e6" - url: "https://github.com/rustdesk-org/flutter_improved_scrolling" - source: git - version: "0.0.3" flutter_keyboard_visibility: dependency: "direct main" description: @@ -1277,10 +1268,11 @@ packages: texture_rgba_renderer: dependency: "direct main" description: - name: texture_rgba_renderer - sha256: cb048abdd800468ca40749ca10d1db9d1e6a055d1cde6234c05191293f0c7d61 - url: "https://pub.dev" - source: hosted + path: "." + ref: "42797e0f03141dc2b585f76c64a13974508058b4" + resolved-ref: "42797e0f03141dc2b585f76c64a13974508058b4" + url: "https://github.com/rustdesk-org/flutter_texture_rgba_renderer" + source: git version: "0.0.16" timing: dependency: transitive diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 2c2000e0ed26..4b7688130081 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.3+52 +version: 1.3.4+53 environment: sdk: '^3.1.0' @@ -71,13 +71,6 @@ dependencies: debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^2.0.5 - flutter_improved_scrolling: - # currently, we use flutter 3.10.0+. - # - # for flutter 3.0.5, please use official version(just comment code below). - # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). - git: - url: https://github.com/rustdesk-org/flutter_improved_scrolling uni_links: git: url: https://github.com/rustdesk-org/uni_links @@ -91,7 +84,10 @@ dependencies: password_strength: ^0.2.0 flutter_launcher_icons: ^0.13.1 flutter_keyboard_visibility: ^5.4.0 - texture_rgba_renderer: ^0.0.16 + texture_rgba_renderer: + git: + url: https://github.com/rustdesk-org/flutter_texture_rgba_renderer + ref: 42797e0f03141dc2b585f76c64a13974508058b4 percent_indicator: ^4.2.2 dropdown_button2: ^2.0.0 flutter_gpu_texture_renderer: diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index c082236e3f5d..902d77948e28 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -345,7 +345,7 @@ fn convert_to_tfc_key(key: Key) -> Option { Key::Numpad9 => TFC_Key::N9, Key::Decimal => TFC_Key::NumpadDecimal, Key::Clear => TFC_Key::NumpadClear, - Key::Pause => TFC_Key::PlayPause, + Key::Pause => TFC_Key::Pause, Key::Print => TFC_Key::Print, Key::Snapshot => TFC_Key::PrintScreen, Key::Insert => TFC_Key::Insert, diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 60c8714d8212..31481ca78f23 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -13,22 +13,35 @@ pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP"; pub struct Distro { pub name: String, + pub id: String, pub version_id: String, } impl Distro { fn new() -> Self { + // to-do: + // 1. Remove `run_cmds`, read file once + // 2. Add more distro infos let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release") .unwrap_or_default() .trim() .trim_matches('"') .to_string(); + let id = run_cmds("awk -F'=' '/^ID=/ {print $2}' /etc/os-release") + .unwrap_or_default() + .trim() + .trim_matches('"') + .to_string(); let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release") .unwrap_or_default() .trim() .trim_matches('"') .to_string(); - Self { name, version_id } + Self { + name, + id, + version_id, + } } } diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index 3bf827865dd6..3cada5a19022 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.3" +version = "1.3.4" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index d929024d84eb..3d56472eedc4 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -193,15 +193,11 @@ impl EncoderApi for HwRamEncoder { } fn support_abr(&self) -> bool { - ["qsv", "vaapi", "mediacodec", "videotoolbox"] - .iter() - .all(|&x| !self.config.name.contains(x)) + ["qsv", "vaapi"].iter().all(|&x| !self.config.name.contains(x)) } fn support_changing_quality(&self) -> bool { - ["vaapi", "mediacodec", "videotoolbox"] - .iter() - .all(|&x| !self.config.name.contains(x)) + ["vaapi"].iter().all(|&x| !self.config.name.contains(x)) } fn latency_free(&self) -> bool { diff --git a/res/PKGBUILD b/res/PKGBUILD index 3da7c98da0ab..d4bef334710c 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.3 +pkgver=1.3.4 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 06653a8ce7f0..a18bb2462bfc 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.3 +Version: 1.3.4 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 74115f8877c1..aba6aa21e967 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.3 +Version: 1.3.4 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 3dfc496c2ec8..5f36a8b56005 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.3 +Version: 1.3.4 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch new file mode 100644 index 000000000000..a0b337c5bae5 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch @@ -0,0 +1,84 @@ +From 7f12898fe8fd12c1042c98b34825ab2eda89e54d Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Sun, 24 Nov 2024 12:58:39 +0800 +Subject: [PATCH 1/2] videotoolbox changing bitrate + +Signed-off-by: 21pages +--- + libavcodec/videotoolboxenc.c | 39 ++++++++++++++++++++++++++++++++++++ + 1 file changed, 39 insertions(+) + +diff --git a/libavcodec/videotoolboxenc.c b/libavcodec/videotoolboxenc.c +index 5ea9afee22..89c927cdcc 100644 +--- a/libavcodec/videotoolboxenc.c ++++ b/libavcodec/videotoolboxenc.c +@@ -278,6 +278,8 @@ typedef struct VTEncContext { + int max_slice_bytes; + int power_efficient; + int max_ref_frames; ++ ++ int last_bit_rate; + } VTEncContext; + + static int vt_dump_encoder(AVCodecContext *avctx) +@@ -1174,6 +1176,7 @@ static int vtenc_create_encoder(AVCodecContext *avctx, + int64_t one_second_value = 0; + void *nums[2]; + ++ vtctx->last_bit_rate = bit_rate; + int status = VTCompressionSessionCreate(kCFAllocatorDefault, + avctx->width, + avctx->height, +@@ -2618,6 +2621,41 @@ static int vtenc_send_frame(AVCodecContext *avctx, + return 0; + } + ++static void update_config(AVCodecContext *avctx) ++{ ++ VTEncContext *vtctx = avctx->priv_data; ++ ++ if (avctx->codec_id != AV_CODEC_ID_PRORES) { ++ if (avctx->bit_rate != vtctx->last_bit_rate) { ++ av_log(avctx, AV_LOG_INFO, "Setting bit rate to %d\n", avctx->bit_rate); ++ vtctx->last_bit_rate = avctx->bit_rate; ++ SInt32 bit_rate = avctx->bit_rate; ++ CFNumberRef bit_rate_num = CFNumberCreate(kCFAllocatorDefault, ++ kCFNumberSInt32Type, ++ &bit_rate); ++ if (!bit_rate_num) return; ++ ++ if (vtctx->constant_bit_rate) { ++ int status = VTSessionSetProperty(vtctx->session, ++ compat_keys.kVTCompressionPropertyKey_ConstantBitRate, ++ bit_rate_num); ++ if (status == kVTPropertyNotSupportedErr) { ++ av_log(avctx, AV_LOG_ERROR, "Error: -constant_bit_rate true is not supported by the encoder.\n"); ++ } ++ } else { ++ int status = VTSessionSetProperty(vtctx->session, ++ kVTCompressionPropertyKey_AverageBitRate, ++ bit_rate_num); ++ if (!status) { ++ av_log(avctx, AV_LOG_ERROR, "Error: cannot set average bit rate: %d\n", status); ++ } ++ } ++ ++ CFRelease(bit_rate_num); ++ } ++ } ++} ++ + static av_cold int vtenc_frame( + AVCodecContext *avctx, + AVPacket *pkt, +@@ -2630,6 +2668,7 @@ static av_cold int vtenc_frame( + CMSampleBufferRef buf = NULL; + ExtraSEI *sei = NULL; + ++ update_config(avctx); + if (frame) { + status = vtenc_send_frame(avctx, vtctx, frame); + +-- +2.43.0.windows.1 + diff --git a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch new file mode 100644 index 000000000000..1f70a5659308 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch @@ -0,0 +1,264 @@ +From 51ac90d8084f7b153eac5133765fa9d0365aa239 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Sun, 24 Nov 2024 14:17:39 +0800 +Subject: [PATCH 1/4] mediacodec changing bitrate + +Signed-off-by: 21pages +--- + libavcodec/mediacodec_wrapper.c | 101 ++++++++++++++++++++++++++++++++ + libavcodec/mediacodec_wrapper.h | 7 +++ + libavcodec/mediacodecenc.c | 18 ++++++ + 3 files changed, 126 insertions(+) + +diff --git a/libavcodec/mediacodec_wrapper.c b/libavcodec/mediacodec_wrapper.c +index 306359071e..1ab4e673f6 100644 +--- a/libavcodec/mediacodec_wrapper.c ++++ b/libavcodec/mediacodec_wrapper.c +@@ -35,6 +35,8 @@ + #include "ffjni.h" + #include "mediacodec_wrapper.h" + ++#define PARAMETER_KEY_VIDEO_BITRATE "video-bitrate" ++ + struct JNIAMediaCodecListFields { + + jclass mediacodec_list_class; +@@ -195,6 +197,8 @@ struct JNIAMediaCodecFields { + jmethodID set_input_surface_id; + jmethodID signal_end_of_input_stream_id; + ++ jmethodID set_parameters_id; ++ + jclass mediainfo_class; + + jmethodID init_id; +@@ -248,6 +252,8 @@ static const struct FFJniField jni_amediacodec_mapping[] = { + { "android/media/MediaCodec", "setInputSurface", "(Landroid/view/Surface;)V", FF_JNI_METHOD, OFFSET(set_input_surface_id), 0 }, + { "android/media/MediaCodec", "signalEndOfInputStream", "()V", FF_JNI_METHOD, OFFSET(signal_end_of_input_stream_id), 0 }, + ++ { "android/media/MediaCodec", "setParameters", "(Landroid/os/Bundle;)V", FF_JNI_METHOD, OFFSET(set_parameters_id), 0 }, ++ + { "android/media/MediaCodec$BufferInfo", NULL, NULL, FF_JNI_CLASS, OFFSET(mediainfo_class), 1 }, + + { "android/media/MediaCodec.BufferInfo", "", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 }, +@@ -292,6 +298,24 @@ typedef struct FFAMediaCodecJni { + + static const FFAMediaCodec media_codec_jni; + ++struct JNIABundleFields ++{ ++ jclass bundle_class; ++ jmethodID init_id; ++ jmethodID put_int_id; ++}; ++ ++#define OFFSET(x) offsetof(struct JNIABundleFields, x) ++static const struct FFJniField jni_abundle_mapping[] = { ++ { "android/os/Bundle", NULL, NULL, FF_JNI_CLASS, OFFSET(bundle_class), 1 }, ++ ++ { "android/os/Bundle", "", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 }, ++ { "android/os/Bundle", "putInt", "(Ljava/lang/String;I)V", FF_JNI_METHOD, OFFSET(put_int_id), 1 }, ++ ++ { NULL } ++}; ++#undef OFFSET ++ + #define JNI_GET_ENV_OR_RETURN(env, log_ctx, ret) do { \ + (env) = ff_jni_get_env(log_ctx); \ + if (!(env)) { \ +@@ -1761,6 +1785,69 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx) + return 0; + } + ++static int mediacodec_jni_setParameter(FFAMediaCodec *ctx, const char* name, int value) ++{ ++ JNIEnv *env = NULL; ++ struct JNIABundleFields jfields = { 0 }; ++ jobject object = NULL; ++ jstring key = NULL; ++ FFAMediaCodecJni *codec = (FFAMediaCodecJni *)ctx; ++ void *log_ctx = codec; ++ int ret = -1; ++ ++ JNI_GET_ENV_OR_RETURN(env, codec, AVERROR_EXTERNAL); ++ ++ if (ff_jni_init_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx) < 0) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to init jfields\n"); ++ goto fail; ++ } ++ ++ object = (*env)->NewObject(env, jfields.bundle_class, jfields.init_id); ++ if (!object) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to create bundle object\n"); ++ goto fail; ++ } ++ ++ key = ff_jni_utf_chars_to_jstring(env, name, log_ctx); ++ if (!key) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to convert key to jstring\n"); ++ goto fail; ++ } ++ ++ (*env)->CallVoidMethod(env, object, jfields.put_int_id, key, value); ++ if (ff_jni_exception_check(env, 1, log_ctx) < 0) { ++ goto fail; ++ } ++ ++ if (!codec->jfields.set_parameters_id) { ++ av_log(log_ctx, AV_LOG_ERROR, "System doesn't support setParameters\n"); ++ goto fail; ++ } ++ ++ (*env)->CallVoidMethod(env, codec->object, codec->jfields.set_parameters_id, object); ++ if (ff_jni_exception_check(env, 1, log_ctx) < 0) { ++ goto fail; ++ } ++ ++ ret = 0; ++ ++fail: ++ if (key) { ++ (*env)->DeleteLocalRef(env, key); ++ } ++ if (object) { ++ (*env)->DeleteLocalRef(env, object); ++ } ++ ff_jni_reset_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx); ++ ++ return ret; ++} ++ ++static int mediacodec_jni_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate) ++{ ++ return mediacodec_jni_setParameter(ctx, PARAMETER_KEY_VIDEO_BITRATE, bitrate); ++} ++ + static const FFAMediaFormat media_format_jni = { + .class = &amediaformat_class, + +@@ -1820,6 +1907,8 @@ static const FFAMediaCodec media_codec_jni = { + .getConfigureFlagEncode = mediacodec_jni_getConfigureFlagEncode, + .cleanOutputBuffers = mediacodec_jni_cleanOutputBuffers, + .signalEndOfInputStream = mediacodec_jni_signalEndOfInputStream, ++ ++ .setDynamicBitrate = mediacodec_jni_setDynamicBitrate, + }; + + typedef struct FFAMediaFormatNdk { +@@ -1893,6 +1982,8 @@ typedef struct FFAMediaCodecNdk { + // Available since API level 26. + media_status_t (*setInputSurface)(AMediaCodec*, ANativeWindow *); + media_status_t (*signalEndOfInputStream)(AMediaCodec *); ++ ++ media_status_t (*setParameters)(AMediaCodec *, const AMediaFormat *format); + } FFAMediaCodecNdk; + + static const FFAMediaFormat media_format_ndk; +@@ -2154,6 +2245,8 @@ static inline FFAMediaCodec *ndk_codec_create(int method, const char *arg) { + GET_SYMBOL(setInputSurface, 0) + GET_SYMBOL(signalEndOfInputStream, 0) + ++ GET_SYMBOL(setParameters, 0) ++ + #undef GET_SYMBOL + + switch (method) { +@@ -2428,6 +2521,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) + return 0; + } + ++static int mediacodec_ndk_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate) ++{ ++ av_log(ctx, AV_LOG_ERROR, "ndk setDynamicBitrate unavailable\n"); ++ return -1; ++} ++ + static const FFAMediaFormat media_format_ndk = { + .class = &amediaformat_ndk_class, + +@@ -2489,6 +2588,8 @@ static const FFAMediaCodec media_codec_ndk = { + .getConfigureFlagEncode = mediacodec_ndk_getConfigureFlagEncode, + .cleanOutputBuffers = mediacodec_ndk_cleanOutputBuffers, + .signalEndOfInputStream = mediacodec_ndk_signalEndOfInputStream, ++ ++ .setDynamicBitrate = mediacodec_ndk_setDynamicBitrate, + }; + + FFAMediaFormat *ff_AMediaFormat_new(int ndk) +diff --git a/libavcodec/mediacodec_wrapper.h b/libavcodec/mediacodec_wrapper.h +index 11a4260497..86c64556ad 100644 +--- a/libavcodec/mediacodec_wrapper.h ++++ b/libavcodec/mediacodec_wrapper.h +@@ -219,6 +219,8 @@ struct FFAMediaCodec { + + // For encoder with FFANativeWindow as input. + int (*signalEndOfInputStream)(FFAMediaCodec *); ++ ++ int (*setDynamicBitrate)(FFAMediaCodec *codec, int bitrate); + }; + + static inline char *ff_AMediaCodec_getName(FFAMediaCodec *codec) +@@ -343,6 +345,11 @@ static inline int ff_AMediaCodec_signalEndOfInputStream(FFAMediaCodec *codec) + return codec->signalEndOfInputStream(codec); + } + ++static inline int ff_AMediaCodec_setDynamicBitrate(FFAMediaCodec *codec, int bitrate) ++{ ++ return codec->setDynamicBitrate(codec, bitrate); ++} ++ + int ff_Build_SDK_INT(AVCodecContext *avctx); + + enum FFAMediaFormatColorRange { +diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c +index d3bf27cb7f..621529d686 100644 +--- a/libavcodec/mediacodecenc.c ++++ b/libavcodec/mediacodecenc.c +@@ -73,6 +73,8 @@ typedef struct MediaCodecEncContext { + int bitrate_mode; + int level; + int pts_as_dts; ++ ++ int last_bit_rate; + } MediaCodecEncContext; + + enum { +@@ -155,6 +157,8 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) + int ret; + int gop; + ++ s->last_bit_rate = avctx->bit_rate; ++ + if (s->use_ndk_codec < 0) + s->use_ndk_codec = !av_jni_get_java_vm(avctx); + +@@ -515,12 +519,26 @@ static int mediacodec_send(AVCodecContext *avctx, + return 0; + } + ++static void update_config(AVCodecContext *avctx) ++{ ++ MediaCodecEncContext *s = avctx->priv_data; ++ if (avctx->bit_rate != s->last_bit_rate) { ++ s->last_bit_rate = avctx->bit_rate; ++ if (0 != ff_AMediaCodec_setDynamicBitrate(s->codec, avctx->bit_rate)) { ++ av_log(avctx, AV_LOG_ERROR, "Failed to set bitrate to %d\n", avctx->bit_rate); ++ } else { ++ av_log(avctx, AV_LOG_INFO, "Set bitrate to %d\n", avctx->bit_rate); ++ } ++ } ++} ++ + static int mediacodec_encode(AVCodecContext *avctx, AVPacket *pkt) + { + MediaCodecEncContext *s = avctx->priv_data; + int ret; + int got_packet = 0; + ++ update_config(avctx); + // Return on three case: + // 1. Serious error + // 2. Got a packet success +-- +2.43.0.windows.1 + diff --git a/res/vcpkg/ffmpeg/portfile.cmake b/res/vcpkg/ffmpeg/portfile.cmake index 3d4c10906dfa..d56475c059f8 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -13,6 +13,8 @@ vcpkg_from_github( patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch patch/0003-amf-colorspace.patch + patch/0004-videotoolbox-changing-bitrate.patch + patch/0005-mediacodec-changing-bitrate.patch ) if(SOURCE_PATH MATCHES " ") diff --git a/src/client.rs b/src/client.rs index 4e7ad8e67a06..937fdee9d24b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1188,9 +1188,15 @@ impl VideoHandler { pub fn new(format: CodecFormat, _display: usize) -> Self { let luid = Self::get_adapter_luid(); log::info!("new video handler for display #{_display}, format: {format:?}, luid: {luid:?}"); + let rgba_format = + if cfg!(feature = "flutter") && (cfg!(windows) || cfg!(target_os = "linux")) { + ImageFormat::ABGR + } else { + ImageFormat::ARGB + }; VideoHandler { decoder: Decoder::new(format, luid), - rgb: ImageRgb::new(ImageFormat::ARGB, crate::get_dst_align_rgba()), + rgb: ImageRgb::new(rgba_format, crate::get_dst_align_rgba()), texture: Default::default(), recorder: Default::default(), record: false, @@ -3293,6 +3299,7 @@ lazy_static::lazy_static! { ("VK_PRINT", Key::ControlKey(ControlKey::Print)), ("VK_EXECUTE", Key::ControlKey(ControlKey::Execute)), ("VK_SNAPSHOT", Key::ControlKey(ControlKey::Snapshot)), + ("VK_SCROLL", Key::ControlKey(ControlKey::Scroll)), ("VK_INSERT", Key::ControlKey(ControlKey::Insert)), ("VK_DELETE", Key::ControlKey(ControlKey::Delete)), ("VK_HELP", Key::ControlKey(ControlKey::Help)), diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4c875be49b72..4630ac3337d7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,6 +19,7 @@ use hbb_common::allow_err; use hbb_common::{ config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, fs, lazy_static, log, + message_proto::Hash, rendezvous_proto::ConnType, ResultType, }; @@ -2341,6 +2342,25 @@ pub fn main_audio_support_loopback() -> SyncReturn { SyncReturn(is_surpport) } +pub fn get_os_distro_info() -> SyncReturn { + #[cfg(target_os = "linux")] + { + let distro = &hbb_common::platform::linux::DISTRO; + SyncReturn( + serde_json::to_string(&HashMap::from([ + ("name", distro.name.clone()), + ("id", distro.id.clone()), + ("version_id", distro.version_id.clone()), + ])) + .unwrap_or_default(), + ) + } + #[cfg(not(target_os = "linux"))] + { + SyncReturn("".to_owned()) + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 039ad4b114ec..1b20ebc4a99a 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 26281c26b4aa..7c081983782a 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 46126056c9be..3c1d202eeb41 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index d680b66a5e87..120200b3520c 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index d15c1b6ba0e1..901b4cdb90ff 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "上传文件"), ("Clipboard is synchronized", "剪贴板已同步"), ("Update client clipboard", "更新客户端的粘贴板"), + ("Untagged", "无标签"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index c4ff80c7e3ff..25046cbcb40e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 905f31739480..fa3e6f100697 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index ba91471c92d4..482b45bfc79b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Dateien hochladen"), ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), ("Update client clipboard", "Client-Zwischenablage aktualisieren"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 73a306c7c9aa..57984b7288f0 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 83747f03cd48..5fe2c8d3a65b 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ee0ffe569942..4b84b1c0f46a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Subir archivos"), ("Clipboard is synchronized", "Portapapeles sincronizado"), ("Update client clipboard", "Actualizar portapapeles del cliente"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 9f67c12262d7..931a3da2d3e5 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 93f5a60b4ef6..e191a74f09c9 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 33c6b7427c1a..051859f60eab 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 8c8332ad1edc..774e03865c2f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -145,7 +145,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Impossible d'établir une connexion directe"), ("Set Password", "Définir le mot de passe"), ("OS Password", "Mot de passe du système d'exploitation"), - ("install_tip", "Vous utilisez une version non installée. En raison des restrictions UAC, en tant que terminal contrôlé, dans certains cas, il ne sera pas en mesure de contrôler la souris et le clavier ou d'enregistrer l'écran. Veuillez cliquer sur le bouton ci-dessous pour installer RustDesk au système pour éviter la question ci-dessus."), + ("install_tip", "RustDesk n'est pas installé, ce qui peut limiter son utilisation à cause de l'UAC. Cliquez ci-dessous pour l'installer."), ("Click to upgrade", "Cliquer pour mettre à niveau"), ("Click to download", "Cliquer pour télécharger"), ("Click to update", "Cliquer pour mettre à jour"), @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 400b5156b40e..39cd98fc4c10 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index d6389480a04d..d3163ae03c7e 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index fc58fe5a6949..c348db1d1d60 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Fájlok feltöltése"), ("Clipboard is synchronized", "A vágólap szinkronizálva van"), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b488f5740d9e..d52c11a384e0 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index b5a191f093ef..838df4d99726 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "File upload"), ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), ("Update client clipboard", "Aggiorna appunti client"), + ("Untagged", "Senza tag"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 1d0f3b7ea157..14c06e0d59bc 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index f266f2536895..93c174894119 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -653,6 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "폴더 업로드"), ("Upload files", "파일 업로드"), ("Clipboard is synchronized", "클립보드가 동기화됨"), - ("Update client clipboard", ""), + ("Update client clipboard", "클라이언트 클립보드 업데이트"), + ("Untagged", "태그 없음"), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 46733ce713e5..f9ee96ca2518 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 723b46a30a81..31522364c1b5 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 0439a45e65f5..31a8d89c02c8 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -653,6 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Augšupielādēt mapi"), ("Upload files", "Augšupielādēt failus"), ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), - ("Update client clipboard", ""), + ("Update client clipboard", "Atjaunināt klienta starpliktuvi"), + ("Untagged", "Neatzīmēts"), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index c9f3ce243921..00ee513b14c0 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 78c6f77b753a..f1c7ba97f9b1 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Bestanden uploaden"), ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), ("Update client clipboard", "Klembord van client bijwerken"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index b6cc5318ab86..872fd5bf713b 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Wyślij pliki"), ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), ("Update client clipboard", "Uaktualnij schowek klienta"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 3fe7951870a4..ad3a5f83171a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index f382b7aba22d..466952c023e0 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 7aaef0e01c4f..830a26a4929a 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index bcc5ed996740..5979961ddf91 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Загрузить файлы"), ("Clipboard is synchronized", "Буфер обмена синхронизирован"), ("Update client clipboard", "Обновить буфер обмена клиента"), + ("Untagged", "Без метки"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 50ba1aeb080a..d6bd0f7111f4 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 4e52bbe40fcb..3fbf30ba3011 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index abab6acd8936..b68e6a1e03bb 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 96bf3e1e06da..9def539e59a9 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 69806fa7f6f9..af00b788ed73 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 1b0cf69e4f25..ce3e99abdee0 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a657201e993b..09fde8671f81 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 1b7b783d30f7..b7f9750f1e8b 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index fb9259ef9b67..8a42a331182f 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -653,6 +653,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上傳資料夾"), ("Upload files", "上傳檔案"), ("Clipboard is synchronized", "剪貼簿已同步"), - ("Update client clipboard", ""), + ("Update client clipboard", "更新客戶端的剪貼簿"), + ("Untagged", "無標籤"), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 3ef8f4c6fdde..6b5ec4381887 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -147,7 +147,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Пароль ОС"), ("install_tip", "Через UAC, в деяких випадках RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натисніть кнопку нижче для встановлення RustDesk в системі"), ("Click to upgrade", "Натисніть, щоб перевірити наявність оновлень"), - ("Click to download", "Натисніть, щоб завантажити"), + ("Click to download", "Натисніть, щоб отримати"), ("Click to update", "Натисніть, щоб оновити"), ("Configure", "Налаштувати"), ("config_acc", "Для віддаленого керування вашою стільницею, вам необхідно надати RustDesk дозволи \"Спеціальні можливості\""), @@ -246,7 +246,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Paste", "Вставити"), ("Paste here?", "Вставити сюди?"), ("Are you sure to close the connection?", "Ви впевнені, що хочете завершити підключення?"), - ("Download new version", "Завантажте нову версію"), + ("Download new version", "Отримайте нову версію"), ("Touch mode", "Сенсорний режим"), ("Mouse mode", "Режим миші"), ("One-Finger Tap", "Дотик одним пальцем"), @@ -307,7 +307,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", "Ігнорувати оптимізації батареї"), ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), ("Start on boot", "Автозапуск"), - ("Start the screen sharing service on boot, requires special permissions", "Запустити службу службу спільного доступу до екрана під час завантаження, потребує спеціальних дозволів"), + ("Start the screen sharing service on boot, requires special permissions", "Запускати службу спільного доступу до екрана під час завантаження, потребує спеціальних дозволів"), ("Connection not allowed", "Підключення не дозволено"), ("Legacy mode", "Застарілий режим"), ("Map mode", "Режим карти"), @@ -648,11 +648,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "На стороні, що керується, увімкнено односторонню передачу файлів."), ("Authentication Required", "Потрібна автентифікація"), ("Authenticate", "Автентифікувати"), - ("web_id_input_tip", "Ви можете ввести ID з того самого серверу, прямий IP-доступ у веб-клієнті не підтримується.\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", для публічного сервера ключ не потрібен."), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), + ("web_id_input_tip", "Ви можете ввести ID на тому самому серверу, прямий IP-доступ у веб-клієнті не підтримується.\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>). Наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\". Для публічного сервера ключ не потрібен."), + ("Download", "Отримати"), + ("Upload folder", "Надіслати теку"), + ("Upload files", "Надіслати файли"), + ("Clipboard is synchronized", "Буфер обміну синхронізовано"), + ("Update client clipboard", "Оновити буфер обміну клієнта"), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 0d4751cd4f91..1970e17ca9c9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -654,5 +654,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", ""), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), + ("Untagged", ""), ].iter().cloned().collect(); } diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 9ee3c1f5c9a6..04095a2d63f0 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -222,6 +222,11 @@ extern "C" return IsWindowsServer(); } + bool is_windows_10_or_greater() + { + return IsWindows10OrGreater(); + } + HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user, DWORD *pDwTokenPid) { HANDLE hProcess = NULL; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index b8e9a979ba8f..0332c5b941e8 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -479,6 +479,7 @@ extern "C" { fn selectInputDesktop() -> BOOL; fn inputDesktopSelected() -> BOOL; fn is_windows_server() -> BOOL; + fn is_windows_10_or_greater() -> BOOL; fn handleMask( out: *mut u8, mask: *const u8, @@ -1560,6 +1561,11 @@ pub fn is_win_server() -> bool { unsafe { is_windows_server() > 0 } } +#[inline] +pub fn is_win_10_or_greater() -> bool { + unsafe { is_windows_10_or_greater() > 0 } +} + pub fn bootstrap() { if let Ok(lic) = get_license_from_exe_name() { *config::EXE_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); diff --git a/src/server/uinput.rs b/src/server/uinput.rs index 60c647862ad7..894ce82f90d7 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -239,7 +239,7 @@ pub mod service { (enigo::Key::Select, evdev::Key::KEY_SELECT), (enigo::Key::Print, evdev::Key::KEY_PRINT), // (enigo::Key::Execute, evdev::Key::KEY_EXECUTE), - // (enigo::Key::Snapshot, evdev::Key::KEY_SNAPSHOT), + (enigo::Key::Snapshot, evdev::Key::KEY_SYSRQ), (enigo::Key::Insert, evdev::Key::KEY_INSERT), (enigo::Key::Help, evdev::Key::KEY_HELP), (enigo::Key::Sleep, evdev::Key::KEY_SLEEP), @@ -247,7 +247,7 @@ pub mod service { (enigo::Key::Scroll, evdev::Key::KEY_SCROLLLOCK), (enigo::Key::NumLock, evdev::Key::KEY_NUMLOCK), (enigo::Key::RWin, evdev::Key::KEY_RIGHTMETA), - (enigo::Key::Apps, evdev::Key::KEY_CONTEXT_MENU), + (enigo::Key::Apps, evdev::Key::KEY_COMPOSE), // it's a little strange that the key is mapped to KEY_COMPOSE, not KEY_MENU (enigo::Key::Multiply, evdev::Key::KEY_KPASTERISK), (enigo::Key::Add, evdev::Key::KEY_KPPLUS), (enigo::Key::Subtract, evdev::Key::KEY_KPMINUS), diff --git a/src/ui_interface.rs b/src/ui_interface.rs index caff46b84f33..5d7f9ee039cd 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -173,21 +173,36 @@ pub fn get_option>(key: T) -> String { } #[inline] -#[cfg(target_os = "macos")] pub fn use_texture_render() -> bool { - cfg!(feature = "flutter") && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y" -} + #[cfg(target_os = "android")] + return false; + #[cfg(target_os = "ios")] + return false; -#[inline] -#[cfg(any(target_os = "windows", target_os = "linux"))] -pub fn use_texture_render() -> bool { - cfg!(feature = "flutter") && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N" -} + #[cfg(target_os = "macos")] + return cfg!(feature = "flutter") + && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; -#[inline] -#[cfg(any(target_os = "android", target_os = "ios"))] -pub fn use_texture_render() -> bool { - false + #[cfg(target_os = "linux")] + return cfg!(feature = "flutter") + && LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N"; + + #[cfg(target_os = "windows")] + { + if !cfg!(feature = "flutter") { + return false; + } + // https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 + #[cfg(debug_assertions)] + let default_texture = true; + #[cfg(not(debug_assertions))] + let default_texture = crate::platform::is_win_10_or_greater(); + if default_texture { + LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) != "N" + } else { + return LocalConfig::get_option(config::keys::OPTION_TEXTURE_RENDER) == "Y"; + } + } } #[inline]