diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 69f7df67b..3d377c8d6 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -428,6 +428,13 @@ jobs: prefix-key: rustdesk-lib-cache key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/ios/Runner/bridge_generated.h + - name: Build rustdesk lib env: VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg @@ -439,7 +446,9 @@ jobs: shell: bash run: | pushd flutter - flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign + # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign + # for easy debugging + flutter build ipa --release --no-codesign # - name: Upload Artifacts # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' diff --git a/flutter/build_ios.sh b/flutter/build_ios.sh index 6d0d627ac..a6468a0a8 100755 --- a/flutter/build_ios.sh +++ b/flutter/build_ios.sh @@ -1,2 +1,5 @@ #!/usr/bin/env bash -flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info +# https://docs.flutter.dev/deployment/ios +# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info +# no obfuscate, because no easy to check errors +flutter build ipa --release diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock index 76d0bac73..1ad5f6360 100644 --- a/flutter/ios/Podfile.lock +++ b/flutter/ios/Podfile.lock @@ -75,7 +75,7 @@ DEPENDENCIES: - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) @@ -106,7 +106,7 @@ EXTERNAL SOURCES: package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/ios" + :path: ".symlinks/plugins/path_provider_foundation/darwin" qr_code_scanner: :path: ".symlinks/plugins/qr_code_scanner/ios" sqflite: @@ -141,6 +141,6 @@ SPEC CHECKSUMS: video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f -PODFILE CHECKSUM: c649b4e69a3086d323110011d04604e416ad0dcd +PODFILE CHECKSUM: 2aff76ba0ac13439479560d1d03e9b4479f5c9e1 -COCOAPODS: 1.12.0 +COCOAPODS: 1.12.1 diff --git a/flutter/ios/Runner.xcodeproj/project.pbxproj b/flutter/ios/Runner.xcodeproj/project.pbxproj index a3bc7d43d..0813abb11 100644 --- a/flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/flutter/ios/Runner.xcodeproj/project.pbxproj @@ -208,6 +208,7 @@ files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -437,6 +438,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_STYLE = "non-global"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -634,6 +636,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_STYLE = "non-global"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -723,6 +726,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_STYLE = "non-global"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/flutter/ios/Runner/AppDelegate.swift b/flutter/ios/Runner/AppDelegate.swift index 06a9a2695..730c9adcb 100644 --- a/flutter/ios/Runner/AppDelegate.swift +++ b/flutter/ios/Runner/AppDelegate.swift @@ -13,9 +13,7 @@ import Flutter } public func dummyMethodToEnforceBundling() { - get_rgba(); - // free_rgba(nil); - // get_by_name("", ""); - // set_by_name("", ""); + dummy_method_to_enforce_bundling(); + session_get_rgba(nil); } } diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index eabd8512d..53611299a 100644 --- a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1,122 @@ { - "images": [ + "images" : [ { - "filename": "Icon-App-20x20@2x.png", - "idiom": "iphone", - "scale": "2x", - "size": "20x20" + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" }, { - "filename": "Icon-App-20x20@3x.png", - "idiom": "iphone", - "scale": "3x", - "size": "20x20" + "filename" : "Icon-App-20x20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" }, { - "filename": "Icon-App-29x29@1x.png", - "idiom": "iphone", - "scale": "1x", - "size": "29x29" + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" }, { - "filename": "Icon-App-29x29@2x.png", - "idiom": "iphone", - "scale": "2x", - "size": "29x29" + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" }, { - "filename": "Icon-App-29x29@3x.png", - "idiom": "iphone", - "scale": "3x", - "size": "29x29" + "filename" : "Icon-App-29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" }, { - "filename": "Icon-App-40x40@2x.png", - "idiom": "iphone", - "scale": "2x", - "size": "40x40" + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" }, { - "filename": "Icon-App-40x40@3x.png", - "idiom": "iphone", - "scale": "3x", - "size": "40x40" + "filename" : "Icon-App-40x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" }, { - "filename": "Icon-App-60x60@2x.png", - "idiom": "iphone", - "scale": "2x", - "size": "60x60" + "filename" : "Icon-App-60x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" }, { - "filename": "Icon-App-60x60@3x.png", - "idiom": "iphone", - "scale": "3x", - "size": "60x60" + "filename" : "Icon-App-60x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" }, { - "filename": "Icon-App-20x20@1x.png", - "idiom": "ipad", - "scale": "1x", - "size": "20x20" + "filename" : "Icon-App-20x20@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" }, { - "filename": "Icon-App-20x20@2x.png", - "idiom": "ipad", - "scale": "2x", - "size": "20x20" + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" }, { - "filename": "Icon-App-29x29@1x.png", - "idiom": "ipad", - "scale": "1x", - "size": "29x29" + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" }, { - "filename": "Icon-App-29x29@2x.png", - "idiom": "ipad", - "scale": "2x", - "size": "29x29" + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" }, { - "filename": "Icon-App-40x40@1x.png", - "idiom": "ipad", - "scale": "1x", - "size": "40x40" + "filename" : "Icon-App-40x40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" }, { - "filename": "Icon-App-40x40@2x.png", - "idiom": "ipad", - "scale": "2x", - "size": "40x40" + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" }, { - "filename": "Icon-App-76x76@1x.png", - "idiom": "ipad", - "scale": "1x", - "size": "76x76" + "filename" : "Icon-App-76x76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" }, { - "filename": "Icon-App-76x76@2x.png", - "idiom": "ipad", - "scale": "2x", - "size": "76x76" + "filename" : "Icon-App-76x76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" }, { - "filename": "Icon-App-83.5x83.5@2x.png", - "idiom": "ipad", - "scale": "2x", - "size": "83.5x83.5" + "filename" : "Icon-App-83.5x83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" }, { - "filename": "Icon-App-1024x1024@1x.png", - "idiom": "ios-marketing", - "scale": "1x", - "size": "1024x1024" + "filename" : "Icon-App-1024x1024@1x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" } ], - "info": { - "author": "icons_launcher", - "version": 1 + "info" : { + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json index 0bedcf2fd..00cabce83 100644 --- a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -1,23 +1,23 @@ { "images" : [ { - "idiom" : "universal", "filename" : "LaunchImage.png", + "idiom" : "universal", "scale" : "1x" }, { - "idiom" : "universal", "filename" : "LaunchImage@2x.png", + "idiom" : "universal", "scale" : "2x" }, { - "idiom" : "universal", "filename" : "LaunchImage@3x.png", + "idiom" : "universal", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } } diff --git a/flutter/ios/Runner/Base.lproj/Main.storyboard b/flutter/ios/Runner/Base.lproj/Main.storyboard index f3c28516f..d68a3a7a5 100644 --- a/flutter/ios/Runner/Base.lproj/Main.storyboard +++ b/flutter/ios/Runner/Base.lproj/Main.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -14,13 +16,14 @@ - + - + + diff --git a/flutter/ios/Runner/Runner-Bridging-Header.h b/flutter/ios/Runner/Runner-Bridging-Header.h index a8c447418..e930a3997 100644 --- a/flutter/ios/Runner/Runner-Bridging-Header.h +++ b/flutter/ios/Runner/Runner-Bridging-Header.h @@ -1,3 +1,3 @@ #import "GeneratedPluginRegistrant.h" -#import "ffi.h" +#import "bridge_generated.h" diff --git a/flutter/ios/Runner/ffi.h b/flutter/ios/Runner/ffi.h deleted file mode 100644 index 701ec4b09..000000000 --- a/flutter/ios/Runner/ffi.h +++ /dev/null @@ -1,4 +0,0 @@ -void* get_rgba(); -void free_rgba(void*); -void set_by_name(const char*, const char*); -const char* get_by_name(const char*, const char*); diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 880d80baa..a2a4e2b23 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -1225,76 +1225,9 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async { final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId); qualityInitValue = quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; - const qualityMinValue = 10.0; - const qualityMoreThresholdValue = 100.0; - const qualityMaxValue = 2000.0; - if (qualityInitValue < qualityMinValue) { - qualityInitValue = qualityMinValue; + if (qualityInitValue < 10 || qualityInitValue > 2000) { + qualityInitValue = 50; } - if (qualityInitValue > qualityMaxValue) { - qualityInitValue = qualityMaxValue; - } - final RxDouble qualitySliderValue = RxDouble(qualityInitValue); - final moreQualityInitValue = qualityInitValue > qualityMoreThresholdValue; - final RxBool moreQualityChecked = RxBool(moreQualityInitValue); - final debouncerQuality = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(quality: v); - }, - initialValue: qualityInitValue, - ); - final qualitySlider = Obx(() => Row( - children: [ - Expanded( - flex: 3, - child: Slider( - value: qualitySliderValue.value, - min: qualityMinValue, - max: moreQualityChecked.value - ? qualityMaxValue - : qualityMoreThresholdValue, - divisions: 18, - onChanged: (double value) { - qualitySliderValue.value = value; - debouncerQuality.value = value; - }, - )), - Expanded( - flex: 1, - child: Text( - '${qualitySliderValue.value.round()}%', - style: const TextStyle(fontSize: 15), - )), - Expanded( - flex: 1, - child: Text( - translate('Bitrate'), - style: const TextStyle(fontSize: 15), - )), - Expanded( - flex: 1, - child: Row( - children: [ - Checkbox( - value: moreQualityChecked.value, - onChanged: (bool? value) { - moreQualityChecked.value = value!; - if (!value && - qualitySliderValue.value > - qualityMoreThresholdValue) { - qualitySliderValue.value = qualityMoreThresholdValue; - debouncerQuality.value = qualityMoreThresholdValue; - } - }, - ).marginOnly(right: 5), - Expanded( - child: Text(translate('More')), - ) - ], - )), - ], - )); // fps final fpsOption = await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps'); @@ -1302,55 +1235,20 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async { if (fpsInitValue < 5 || fpsInitValue > 120) { fpsInitValue = 30; } - final RxDouble fpsSliderValue = RxDouble(fpsInitValue); - final debouncerFps = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(fps: v); - }, - initialValue: qualityInitValue, - ); bool? direct; try { direct = ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect; } catch (_) {} - final fpsSlider = Offstage( - offstage: (await bind.mainIsUsingPublicServer() && direct != true) || - version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0, - child: Row( - children: [ - Expanded( - flex: 3, - child: Obx((() => Slider( - value: fpsSliderValue.value, - min: 5, - max: 120, - divisions: 23, - onChanged: (double value) { - fpsSliderValue.value = value; - debouncerFps.value = value; - }, - )))), - Expanded( - flex: 1, - child: Obx(() => Text( - '${fpsSliderValue.value.round()}', - style: const TextStyle(fontSize: 15), - ))), - Expanded( - flex: 2, - child: Text( - translate('FPS'), - style: const TextStyle(fontSize: 15), - )) - ], - ), - ); + bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) || + version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0; - final content = Column( - children: [qualitySlider, fpsSlider], - ); + final content = customImageQualityWidget( + initQuality: qualityInitValue, + initFps: fpsInitValue, + setQuality: (v) => setCustomValues(quality: v), + setFps: (v) => setCustomValues(fps: v), + showFps: !notShowFps); msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); } diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index a3c59d6b1..f2f3b8fb0 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -26,15 +26,31 @@ class DraggableChatWindow extends StatelessWidget { @override Widget build(BuildContext context) { - return Draggable( + return isIOS + ? IOSDraggable ( + position: position, + chatModel: chatModel, + width: width, + height: height, + builder: (context) { + return Column( + children: [ + _buildMobileAppBar(context), + Expanded( + child: ChatPage(chatModel: chatModel), + ), + ], + ); + }, + ) + : Draggable( checkKeyboard: true, position: position, width: width, height: height, builder: (context, onPanUpdate) { - final child = isIOS - ? ChatPage(chatModel: chatModel) - : Scaffold( + final child = + Scaffold( resizeToAvoidBottomInset: false, appBar: CustomAppBar( onPanUpdate: onPanUpdate, @@ -331,6 +347,69 @@ class _DraggableState extends State { } } +class IOSDraggable extends StatefulWidget { + const IOSDraggable({ + Key? key, + this.position = Offset.zero, + required this.chatModel, + required this.width, + required this.height, + required this.builder}) + : super(key: key); + + final Offset position; + final ChatModel chatModel; + final double width; + final double height; + final Widget Function(BuildContext) builder; + + @override + _IOSDraggableState createState() => _IOSDraggableState(); +} + +class _IOSDraggableState extends State { +late Offset _position; +late ChatModel _chatModel; +late double _width; +late double _height; + +@override +void initState() { + super.initState(); + _position = widget.position; + _chatModel = widget.chatModel; + _width = widget.width; + _height = widget.height; +} + +@override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + left: _position.dx, + top: _position.dy, + child: GestureDetector( + onPanUpdate: (details) { + setState(() { + _position += details.delta; + }); + }, + child: Material( + child: + Container( + width: _width, + height: _height, + child: widget.builder(context), + ), + ), + ), + ), + ], + ); + } +} + class QualityMonitor extends StatelessWidget { final QualityMonitorModel qualityMonitorModel; QualityMonitor(this.qualityMonitorModel); diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart new file mode 100644 index 000000000..771b65ab5 --- /dev/null +++ b/flutter/lib/common/widgets/setting_widgets.dart @@ -0,0 +1,277 @@ +import 'package:debounce_throttle/debounce_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; + +customImageQualityWidget( + {required double initQuality, + required double initFps, + required Function(double) setQuality, + required Function(double) setFps, + required bool showFps}) { + final qualityValue = initQuality.obs; + final fpsValue = initFps.obs; + + final RxBool moreQualityChecked = RxBool(qualityValue.value > 100); + final debouncerQuality = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setQuality(v); + }, + initialValue: qualityValue.value, + ); + final debouncerFps = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setFps(v); + }, + initialValue: fpsValue.value, + ); + + onMoreChanged(bool? value) { + if (value == null) return; + moreQualityChecked.value = value; + if (!value && qualityValue.value > 100) { + qualityValue.value = 100; + } + debouncerQuality.value = qualityValue.value; + } + + return Column( + children: [ + Obx(() => Row( + children: [ + Expanded( + flex: 3, + child: Slider( + value: qualityValue.value, + min: 10.0, + max: moreQualityChecked.value ? 2000 : 100, + divisions: moreQualityChecked.value ? 199 : 18, + onChanged: (double value) async { + qualityValue.value = value; + debouncerQuality.value = value; + }, + ), + ), + Expanded( + flex: 1, + child: Text( + '${qualityValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + Expanded( + flex: isMobile ? 2 : 1, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )), + // mobile doesn't have enough space + if (!isMobile) + Expanded( + flex: 1, + child: Row( + children: [ + Checkbox( + value: moreQualityChecked.value, + onChanged: onMoreChanged, + ), + Expanded( + child: Text(translate('More')), + ) + ], + )) + ], + )), + if (isMobile) + Obx(() => Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Checkbox( + value: moreQualityChecked.value, + onChanged: onMoreChanged, + ), + ), + ), + Expanded( + child: Text(translate('More')), + ) + ], + )), + if (showFps) + Obx(() => Row( + children: [ + Expanded( + flex: 3, + child: Slider( + value: fpsValue.value, + min: 5.0, + max: 120.0, + divisions: 23, + onChanged: (double value) async { + fpsValue.value = value; + debouncerFps.value = value; + }, + ), + ), + Expanded( + flex: 1, + child: Text( + '${fpsValue.value.round()}', + style: const TextStyle(fontSize: 15), + )), + Expanded( + flex: 2, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + )), + ], + ); +} + +customImageQualitySetting() { + final qualityKey = 'custom_image_quality'; + final fpsKey = 'custom-fps'; + + var initQuality = + (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? 50.0); + if (initQuality < 10 || initQuality > 2000) { + initQuality = 50; + } + var initFps = + (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0); + if (initFps < 5 || initFps > 120) { + initFps = 30; + } + + return customImageQualityWidget( + initQuality: initQuality, + initFps: initFps, + setQuality: (v) { + bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString()); + }, + setFps: (v) { + bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString()); + }, + showFps: true); +} + +Future setServerConfig( + List controllers, + List errMsgs, + ServerConfig config, +) async { + config.idServer = config.idServer.trim(); + config.relayServer = config.relayServer.trim(); + config.apiServer = config.apiServer.trim(); + config.key = config.key.trim(); + // id + if (config.idServer.isNotEmpty) { + errMsgs[0].value = + translate(await bind.mainTestIfValidServer(server: config.idServer)); + if (errMsgs[0].isNotEmpty) { + return false; + } + } + // relay + if (config.relayServer.isNotEmpty) { + errMsgs[1].value = + translate(await bind.mainTestIfValidServer(server: config.relayServer)); + if (errMsgs[1].isNotEmpty) { + return false; + } + } + // api + if (config.apiServer.isNotEmpty) { + if (!config.apiServer.startsWith('http://') && + !config.apiServer.startsWith('https://')) { + errMsgs[2].value = + '${translate("API Server")}: ${translate("invalid_http")}'; + return false; + } + } + final oldApiServer = await bind.mainGetApiServer(); + + // should set one by one + await bind.mainSetOption( + key: 'custom-rendezvous-server', value: config.idServer); + await bind.mainSetOption(key: 'relay-server', value: config.relayServer); + await bind.mainSetOption(key: 'api-server', value: config.apiServer); + await bind.mainSetOption(key: 'key', value: config.key); + + final newApiServer = await bind.mainGetApiServer(); + if (oldApiServer.isNotEmpty && + oldApiServer != newApiServer && + gFFI.userModel.isLogin) { + gFFI.userModel.logOut(apiServer: oldApiServer); + } + return true; +} + +List ServerConfigImportExportWidgets( + List controllers, + List errMsgs, +) { + import() { + Clipboard.getData(Clipboard.kTextPlain).then((value) { + final text = value?.text; + if (text != null && text.isNotEmpty) { + try { + final sc = ServerConfig.decode(text); + if (sc.idServer.isNotEmpty) { + controllers[0].text = sc.idServer; + controllers[1].text = sc.relayServer; + controllers[2].text = sc.apiServer; + controllers[3].text = sc.key; + Future success = setServerConfig(controllers, errMsgs, sc); + success.then((value) { + if (value) { + showToast( + translate('Import server configuration successfully')); + } else { + showToast(translate('Invalid server configuration')); + } + }); + } else { + showToast(translate('Invalid server configuration')); + } + } catch (e) { + showToast(translate('Invalid server configuration')); + } + } else { + showToast(translate('Clipboard is empty')); + } + }); + } + + export() { + final text = ServerConfig( + idServer: controllers[0].text.trim(), + relayServer: controllers[1].text.trim(), + apiServer: controllers[2].text.trim(), + key: controllers[3].text.trim()) + .encode(); + debugPrint("ServerConfig export: $text"); + Clipboard.setData(ClipboardData(text: text)); + showToast(translate('Export server configuration successfully')); + } + + return [ + Tooltip( + message: translate('Import Server Config'), + child: IconButton( + icon: Icon(Icons.paste, color: Colors.grey), onPressed: import), + ), + Tooltip( + message: translate('Export Server Config'), + child: IconButton( + icon: Icon(Icons.copy, color: Colors.grey), onPressed: export)) + ]; +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 5b317d5f3..3a33c7e5b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; @@ -965,54 +966,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { var relayController = TextEditingController(text: old('relay-server')); var apiController = TextEditingController(text: old('api-server')); var keyController = TextEditingController(text: old('key')); - - set(String idServer, String relayServer, String apiServer, - String key) async { - idServer = idServer.trim(); - relayServer = relayServer.trim(); - apiServer = apiServer.trim(); - key = key.trim(); - if (idServer.isNotEmpty) { - idErrMsg.value = - translate(await bind.mainTestIfValidServer(server: idServer)); - if (idErrMsg.isNotEmpty) { - return false; - } - } - if (relayServer.isNotEmpty) { - relayErrMsg.value = - translate(await bind.mainTestIfValidServer(server: relayServer)); - if (relayErrMsg.isNotEmpty) { - return false; - } - } - if (apiServer.isNotEmpty) { - if (!apiServer.startsWith('http://') && - !apiServer.startsWith('https://')) { - apiErrMsg.value = - '${translate("API Server")}: ${translate("invalid_http")}'; - return false; - } - } - final oldApiServer = await bind.mainGetApiServer(); - - // should set one by one - await bind.mainSetOption( - key: 'custom-rendezvous-server', value: idServer); - await bind.mainSetOption(key: 'relay-server', value: relayServer); - await bind.mainSetOption(key: 'api-server', value: apiServer); - await bind.mainSetOption(key: 'key', value: key); - - final newApiServer = await bind.mainGetApiServer(); - if (oldApiServer.isNotEmpty && oldApiServer != newApiServer) { - await gFFI.userModel.logOut(apiServer: oldApiServer); - } - return true; - } + final controllers = [ + idController, + relayController, + apiController, + keyController, + ]; + final errMsgs = [ + idErrMsg, + relayErrMsg, + apiErrMsg, + ]; submit() async { - bool result = await set(idController.text, relayController.text, - apiController.text, keyController.text); + bool result = await setServerConfig( + controllers, + errMsgs, + ServerConfig( + idServer: idController.text, + relayServer: relayController.text, + apiServer: apiController.text, + key: keyController.text)); if (result) { setState(() {}); showToast(translate('Successful')); @@ -1021,83 +995,28 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } } - import() { - Clipboard.getData(Clipboard.kTextPlain).then((value) { - final text = value?.text; - if (text != null && text.isNotEmpty) { - try { - final sc = ServerConfig.decode(text); - if (sc.idServer.isNotEmpty) { - idController.text = sc.idServer; - relayController.text = sc.relayServer; - apiController.text = sc.apiServer; - keyController.text = sc.key; - Future success = - set(sc.idServer, sc.relayServer, sc.apiServer, sc.key); - success.then((value) { - if (value) { - showToast( - translate('Import server configuration successfully')); - } else { - showToast(translate('Invalid server configuration')); - } - }); - } else { - showToast(translate('Invalid server configuration')); - } - } catch (e) { - showToast(translate('Invalid server configuration')); - } - } else { - showToast(translate('Clipboard is empty')); - } - }); - } - - export() { - final text = ServerConfig( - idServer: idController.text, - relayServer: relayController.text, - apiServer: apiController.text, - key: keyController.text) - .encode(); - debugPrint("ServerConfig export: $text"); - - Clipboard.setData(ClipboardData(text: text)); - showToast(translate('Export server configuration successfully')); - } - bool secure = !enabled; - return _Card(title: 'ID/Relay Server', title_suffix: [ - Tooltip( - message: translate('Import Server Config'), - child: IconButton( - icon: Icon(Icons.paste, color: Colors.grey), - onPressed: enabled ? import : null), - ), - Tooltip( - message: translate('Export Server Config'), - child: IconButton( - icon: Icon(Icons.copy, color: Colors.grey), - onPressed: enabled ? export : null)), - ], children: [ - Column( + return _Card( + title: 'ID/Relay Server', + title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs), children: [ - Obx(() => _LabeledTextField(context, 'ID Server', idController, - idErrMsg.value, enabled, secure)), - Obx(() => _LabeledTextField(context, 'Relay Server', - relayController, relayErrMsg.value, enabled, secure)), - Obx(() => _LabeledTextField(context, 'API Server', apiController, - apiErrMsg.value, enabled, secure)), - _LabeledTextField( - context, 'Key', keyController, '', enabled, secure), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [_Button('Apply', submit, enabled: enabled)], - ).marginOnly(top: 10), - ], - ) - ]); + Column( + children: [ + Obx(() => _LabeledTextField(context, 'ID Server', idController, + idErrMsg.value, enabled, secure)), + Obx(() => _LabeledTextField(context, 'Relay Server', + relayController, relayErrMsg.value, enabled, secure)), + Obx(() => _LabeledTextField(context, 'API Server', + apiController, apiErrMsg.value, enabled, secure)), + _LabeledTextField( + context, 'Key', keyController, '', enabled, secure), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [_Button('Apply', submit, enabled: enabled)], + ).marginOnly(top: 10), + ], + ) + ]); } return tmpWrapper(); @@ -1181,15 +1100,6 @@ class _DisplayState extends State<_Display> { } final groupValue = bind.mainGetUserDefaultOption(key: key); - final qualityKey = 'custom_image_quality'; - final qualityValue = - (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? - 50.0) - .obs; - final fpsKey = 'custom-fps'; - final fpsValue = - (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0) - .obs; return _Card(title: 'Default Image Quality', children: [ _Radio(context, value: kRemoteImageQualityBest, @@ -1213,64 +1123,7 @@ class _DisplayState extends State<_Display> { onChanged: onChanged), Offstage( offstage: groupValue != kRemoteImageQualityCustom, - child: Column( - children: [ - Obx(() => Row( - children: [ - Slider( - value: qualityValue.value, - min: 10.0, - max: 100.0, - divisions: 18, - onChanged: (double value) async { - qualityValue.value = value; - await bind.mainSetUserDefaultOption( - key: qualityKey, value: value.toString()); - }, - ), - SizedBox( - width: 40, - child: Text( - '${qualityValue.value.round()}%', - style: const TextStyle(fontSize: 15), - )), - SizedBox( - width: 50, - child: Text( - translate('Bitrate'), - style: const TextStyle(fontSize: 15), - )) - ], - )), - Obx(() => Row( - children: [ - Slider( - value: fpsValue.value, - min: 5.0, - max: 120.0, - divisions: 23, - onChanged: (double value) async { - fpsValue.value = value; - await bind.mainSetUserDefaultOption( - key: fpsKey, value: value.toString()); - }, - ), - SizedBox( - width: 40, - child: Text( - '${fpsValue.value.round()}', - style: const TextStyle(fontSize: 15), - )), - SizedBox( - width: 50, - child: Text( - translate('FPS'), - style: const TextStyle(fontSize: 15), - )) - ], - )), - ], - ), + child: customImageQualitySetting(), ) ]); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 86bc86517..43273c547 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -398,7 +398,7 @@ class _AppState extends State { themeMode: MyTheme.currentThemeMode(), home: isDesktop ? const DesktopTabPage() - : !isAndroid + : isWeb ? WebHomePage() : HomePage(), localizationsDelegates: const [ diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index c99462166..4a14f8466 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -28,7 +28,7 @@ class ConnectionPage extends StatefulWidget implements PageShape { final title = translate("Connection"); @override - final appBarActions = !isAndroid ? [const WebMenu()] : []; + final appBarActions = isWeb ? [const WebMenu()] : []; @override State createState() => _ConnectionPageState(); @@ -211,25 +211,6 @@ class WebMenu extends StatefulWidget { } class _WebMenuState extends State { - String url = ""; - - @override - void initState() { - super.initState(); - () async { - final urlRes = await bind.mainGetApiServer(); - var update = false; - if (urlRes != url) { - url = urlRes; - update = true; - } - - if (update) { - setState(() {}); - } - }(); - } - @override Widget build(BuildContext context) { Provider.of(context); @@ -251,16 +232,14 @@ class _WebMenuState extends State { child: Text(translate('ID/Relay Server')), ) ] + - (url.contains('admin.rustdesk.com') - ? >[] - : [ - PopupMenuItem( - value: "login", - child: Text(gFFI.userModel.userName.value.isEmpty - ? translate("Login") - : '${translate("Logout")} (${gFFI.userModel.userName.value})'), - ) - ]) + + [ + PopupMenuItem( + value: "login", + child: Text(gFFI.userModel.userName.value.isEmpty + ? translate("Login") + : '${translate("Logout")} (${gFFI.userModel.userName.value})'), + ) + ] + [ PopupMenuItem( value: "about", diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index cac59ec8f..ff650d3c9 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -494,7 +494,7 @@ class _RemotePageState extends State { gFFI.ffiModel.toggleTouchMode(); final v = gFFI.ffiModel.touchMode ? 'Y' : ''; bind.sessionPeerOption( - sessionId: sessionId, name: "touch", value: v); + sessionId: sessionId, name: "touch-mode", value: v); }))); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index e7e6daade..e2120a05f 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:settings_ui/settings_ui.dart'; @@ -383,7 +383,7 @@ class _SettingsState extends State with WidgetsBindingObserver { SettingsSection( title: Text(translate('Account')), tiles: [ - SettingsTile.navigation( + SettingsTile( title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty ? translate('Login') : '${translate('Logout')} (${gFFI.userModel.userName.value})')), @@ -399,19 +399,19 @@ class _SettingsState extends State with WidgetsBindingObserver { ], ), SettingsSection(title: Text(translate("Settings")), tiles: [ - SettingsTile.navigation( + SettingsTile( title: Text(translate('ID/Relay Server')), leading: Icon(Icons.cloud), onPressed: (context) { showServerSettings(gFFI.dialogManager); }), - SettingsTile.navigation( + SettingsTile( title: Text(translate('Language')), leading: Icon(Icons.translate), onPressed: (context) { showLanguageSettings(gFFI.dialogManager); }), - SettingsTile.navigation( + SettingsTile( title: Text(translate( Theme.of(context).brightness == Brightness.light ? 'Dark Theme' @@ -424,45 +424,50 @@ class _SettingsState extends State with WidgetsBindingObserver { }, ) ]), - SettingsSection( - title: Text(translate("Recording")), - tiles: [ - SettingsTile.switchTile( - title: Text(translate('Automatically record incoming sessions')), - leading: Icon(Icons.videocam), - description: FutureBuilder( - builder: (ctx, data) => Offstage( - offstage: !data.hasData, - child: Text("${translate("Directory")}: ${data.data}")), - future: bind.mainDefaultVideoSaveDirectory()), - initialValue: _autoRecordIncomingSession, - onToggle: (v) async { - await bind.mainSetOption( - key: "allow-auto-record-incoming", - value: bool2option("allow-auto-record-incoming", v)); - final newValue = option2bool( - 'allow-auto-record-incoming', - await bind.mainGetOption( - key: 'allow-auto-record-incoming')); - setState(() { - _autoRecordIncomingSession = newValue; - }); - }, - ), - ], - ), - SettingsSection( - title: Text(translate("Share Screen")), - tiles: shareScreenTiles, - ), - SettingsSection( - title: Text(translate("Enhancements")), - tiles: enhancementsTiles, - ), + if (isAndroid) + SettingsSection( + title: Text(translate("Recording")), + tiles: [ + SettingsTile.switchTile( + title: + Text(translate('Automatically record incoming sessions')), + leading: Icon(Icons.videocam), + description: FutureBuilder( + builder: (ctx, data) => Offstage( + offstage: !data.hasData, + child: Text("${translate("Directory")}: ${data.data}")), + future: bind.mainDefaultVideoSaveDirectory()), + initialValue: _autoRecordIncomingSession, + onToggle: (v) async { + await bind.mainSetOption( + key: "allow-auto-record-incoming", + value: bool2option("allow-auto-record-incoming", v)); + final newValue = option2bool( + 'allow-auto-record-incoming', + await bind.mainGetOption( + key: 'allow-auto-record-incoming')); + setState(() { + _autoRecordIncomingSession = newValue; + }); + }, + ), + ], + ), + if (isAndroid) + SettingsSection( + title: Text(translate("Share Screen")), + tiles: shareScreenTiles, + ), + defaultDisplaySection(), + if (isAndroid) + SettingsSection( + title: Text(translate("Enhancements")), + tiles: enhancementsTiles, + ), SettingsSection( title: Text(translate("About")), tiles: [ - SettingsTile.navigation( + SettingsTile( onPressed: (context) async { if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url)); @@ -477,21 +482,22 @@ class _SettingsState extends State with WidgetsBindingObserver { )), ), leading: Icon(Icons.info)), - SettingsTile.navigation( + SettingsTile( title: Text(translate("Build Date")), value: Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Text(_buildDate), ), leading: Icon(Icons.query_builder)), - SettingsTile.navigation( - onPressed: (context) => onCopyFingerprint(_fingerprint), - title: Text(translate("Fingerprint")), - value: Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: Text(_fingerprint), - ), - leading: Icon(Icons.fingerprint)), + if (isAndroid) + SettingsTile( + onPressed: (context) => onCopyFingerprint(_fingerprint), + title: Text(translate("Fingerprint")), + value: Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Text(_fingerprint), + ), + leading: Icon(Icons.fingerprint)), ], ), ], @@ -508,6 +514,23 @@ class _SettingsState extends State with WidgetsBindingObserver { } return true; } + + defaultDisplaySection() { + return SettingsSection( + title: Text(translate("Display Settings")), + tiles: [ + SettingsTile( + title: Text(translate('Display Settings')), + leading: Icon(Icons.desktop_windows_outlined), + trailing: Icon(Icons.arrow_forward_ios), + onPressed: (context) { + Navigator.push(context, MaterialPageRoute(builder: (context) { + return _DisplayPage(); + })); + }) + ], + ); + } } void showServerSettings(OverlayDialogManager dialogManager) async { @@ -618,3 +641,181 @@ class ScanButton extends StatelessWidget { ); } } + +class _DisplayPage extends StatefulWidget { + const _DisplayPage({super.key}); + + @override + State<_DisplayPage> createState() => __DisplayPageState(); +} + +class __DisplayPageState extends State<_DisplayPage> { + @override + Widget build(BuildContext context) { + final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings()); + final h264 = codecsJson['h264'] ?? false; + final h265 = codecsJson['h265'] ?? false; + var codecList = [ + _RadioEntry('Auto', 'auto'), + _RadioEntry('VP8', 'vp8'), + _RadioEntry('VP9', 'vp9'), + _RadioEntry('AV1', 'av1'), + if (h264) _RadioEntry('H264', 'h264'), + if (h265) _RadioEntry('H265', 'h265') + ]; + RxBool showCustomImageQuality = false.obs; + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Icons.arrow_back_ios)), + title: Text(translate('Display Settings')), + centerTitle: true, + ), + body: SettingsList(sections: [ + SettingsSection( + tiles: [ + _getPopupDialogRadioEntry( + title: 'Default View Style', + list: [ + _RadioEntry('Scale original', kRemoteViewStyleOriginal), + _RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive) + ], + getter: () => bind.mainGetUserDefaultOption(key: 'view_style'), + asyncSetter: (value) async { + await bind.mainSetUserDefaultOption( + key: 'view_style', value: value); + }, + ), + _getPopupDialogRadioEntry( + title: 'Default Image Quality', + list: [ + _RadioEntry('Good image quality', kRemoteImageQualityBest), + _RadioEntry('Balanced', kRemoteImageQualityBalanced), + _RadioEntry('Optimize reaction time', kRemoteImageQualityLow), + _RadioEntry('Custom', kRemoteImageQualityCustom), + ], + getter: () { + final v = bind.mainGetUserDefaultOption(key: 'image_quality'); + showCustomImageQuality.value = v == kRemoteImageQualityCustom; + return v; + }, + asyncSetter: (value) async { + await bind.mainSetUserDefaultOption( + key: 'image_quality', value: value); + showCustomImageQuality.value = + value == kRemoteImageQualityCustom; + }, + tail: customImageQualitySetting(), + showTail: showCustomImageQuality, + notCloseValue: kRemoteImageQualityCustom, + ), + _getPopupDialogRadioEntry( + title: 'Default Codec', + list: codecList, + getter: () => + bind.mainGetUserDefaultOption(key: 'codec-preference'), + asyncSetter: (value) async { + await bind.mainSetUserDefaultOption( + key: 'codec-preference', value: value); + }, + ), + ], + ), + SettingsSection( + title: Text(translate('Other Default Options')), + tiles: [ + otherRow('Show remote cursor', 'show_remote_cursor'), + otherRow('Show quality monitor', 'show_quality_monitor'), + otherRow('Mute', 'disable_audio'), + otherRow('Disable clipboard', 'disable_clipboard'), + otherRow('Lock after session end', 'lock_after_session_end'), + otherRow('Privacy mode', 'privacy_mode'), + otherRow('Touch mode', 'touch-mode'), + ], + ), + ]), + ); + } + + otherRow(String label, String key) { + final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; + return SettingsTile.switchTile( + initialValue: value, + title: Text(translate(label)), + onToggle: (b) async { + await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : ''); + setState(() {}); + }, + ); + } +} + +class _RadioEntry { + final String label; + final String value; + _RadioEntry(this.label, this.value); +} + +typedef _RadioEntryGetter = String Function(); +typedef _RadioEntrySetter = Future Function(String); + +_getPopupDialogRadioEntry({ + required String title, + required List<_RadioEntry> list, + required _RadioEntryGetter getter, + required _RadioEntrySetter asyncSetter, + Widget? tail, + RxBool? showTail, + String? notCloseValue, +}) { + RxString groupValue = ''.obs; + RxString valueText = ''.obs; + + init() { + groupValue.value = getter(); + final e = list.firstWhereOrNull((e) => e.value == groupValue.value); + if (e != null) { + valueText.value = e.label; + } + } + + init(); + + void showDialog() async { + gFFI.dialogManager.show((setState, close, context) { + onChanged(String? value) async { + if (value == null) return; + await asyncSetter(value); + init(); + if (value != notCloseValue) { + close(); + } + } + + return CustomAlertDialog( + content: Obx( + () => Column(children: [ + ...list + .map((e) => getRadio(Text(translate(e.label)), e.value, + groupValue.value, (String? value) => onChanged(value))) + .toList(), + Offstage( + offstage: + !(tail != null && showTail != null && showTail.value == true), + child: tail, + ), + ]), + )); + }, backDismiss: true, clickMaskDismiss: true); + } + + return SettingsTile( + title: Text(translate(title)), + onPressed: (context) => showDialog(), + value: Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Obx(() => Text(translate(valueText.value))), + ), + ); +} diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 550b37d72..3e745ecce 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -147,59 +147,72 @@ void setTemporaryPasswordLengthDialog( void showServerSettingsWithValue( ServerConfig serverConfig, OverlayDialogManager dialogManager) async { - Map oldOptions = jsonDecode(await bind.mainGetOptions()); - final oldCfg = ServerConfig.fromOptions(oldOptions); - var isInProgress = false; final idCtrl = TextEditingController(text: serverConfig.idServer); final relayCtrl = TextEditingController(text: serverConfig.relayServer); final apiCtrl = TextEditingController(text: serverConfig.apiServer); final keyCtrl = TextEditingController(text: serverConfig.key); - String? idServerMsg; - String? relayServerMsg; - String? apiServerMsg; + RxString idServerMsg = ''.obs; + RxString relayServerMsg = ''.obs; + RxString apiServerMsg = ''.obs; + + final controllers = [idCtrl, relayCtrl, apiCtrl, keyCtrl]; + final errMsgs = [ + idServerMsg, + relayServerMsg, + apiServerMsg, + ]; dialogManager.show((setState, close, context) { - Future validate() async { - if (idCtrl.text != oldCfg.idServer) { - final res = await validateAsync(idCtrl.text); - setState(() => idServerMsg = res); - if (idServerMsg != null) return false; - } - if (relayCtrl.text != oldCfg.relayServer) { - relayServerMsg = await validateAsync(relayCtrl.text); - if (relayServerMsg != null) return false; - } - if (apiCtrl.text != oldCfg.apiServer) { - if (apiServerMsg != null) return false; - } - return true; + Future submit() async { + setState(() { + isInProgress = true; + }); + bool ret = await setServerConfig( + controllers, + errMsgs, + ServerConfig( + idServer: idCtrl.text.trim(), + relayServer: relayCtrl.text.trim(), + apiServer: apiCtrl.text.trim(), + key: keyCtrl.text.trim())); + setState(() { + isInProgress = false; + }); + return ret; } return CustomAlertDialog( - title: Text(translate('ID/Relay Server')), + title: Row( + children: [ + Expanded(child: Text(translate('ID/Relay Server'))), + ...ServerConfigImportExportWidgets(controllers, errMsgs), + ], + ), content: Form( - child: Column( + child: Obx(() => Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: idCtrl, decoration: InputDecoration( labelText: translate('ID Server'), - errorText: idServerMsg), + errorText: idServerMsg.value.isEmpty + ? null + : idServerMsg.value), + ) + ] + + [ + TextFormField( + controller: relayCtrl, + decoration: InputDecoration( + labelText: translate('Relay Server'), + errorText: relayServerMsg.value.isEmpty + ? null + : relayServerMsg.value), ) ] + - (isAndroid - ? [ - TextFormField( - controller: relayCtrl, - decoration: InputDecoration( - labelText: translate('Relay Server'), - errorText: relayServerMsg), - ) - ] - : []) + [ TextFormField( controller: apiCtrl, @@ -214,7 +227,7 @@ void showServerSettingsWithValue( return translate("invalid_http"); } } - return apiServerMsg; + return null; }, ), TextFormField( @@ -225,7 +238,7 @@ void showServerSettingsWithValue( ), // NOT use Offstage to wrap LinearProgressIndicator if (isInProgress) const LinearProgressIndicator(), - ])), + ]))), actions: [ dialogButton('Cancel', onPressed: () { close(); @@ -233,35 +246,12 @@ void showServerSettingsWithValue( dialogButton( 'OK', onPressed: () async { - setState(() { - idServerMsg = null; - relayServerMsg = null; - apiServerMsg = null; - isInProgress = true; - }); - if (await validate()) { - if (idCtrl.text != oldCfg.idServer) { - if (oldCfg.idServer.isNotEmpty) { - await gFFI.userModel.logOut(); - } - bind.mainSetOption( - key: "custom-rendezvous-server", value: idCtrl.text); - } - if (relayCtrl.text != oldCfg.relayServer) { - bind.mainSetOption(key: "relay-server", value: relayCtrl.text); - } - if (keyCtrl.text != oldCfg.key) { - bind.mainSetOption(key: "key", value: keyCtrl.text); - } - if (apiCtrl.text != oldCfg.apiServer) { - bind.mainSetOption(key: "api-server", value: apiCtrl.text); - } + if (await submit()) { close(); showToast(translate('Successful')); + } else { + showToast(translate('Failed')); } - setState(() { - isInProgress = false; - }); }, ), ], diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 898427351..bffd9d426 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -63,7 +63,7 @@ class ChatModel with ChangeNotifier { bool isConnManager = false; RxBool isWindowFocus = true.obs; - BlockableOverlayState? _blockableOverlayState; + BlockableOverlayState _blockableOverlayState = BlockableOverlayState(); final Rx _voiceCallStatus = Rx(VoiceCallStatus.notStarted); Rx get voiceCallStatus => _voiceCallStatus; @@ -154,7 +154,7 @@ class ChatModel with ChangeNotifier { } } - final overlayState = _blockableOverlayState?.state; + final overlayState = _blockableOverlayState.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 14b6b4df6..95b5094df 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1815,7 +1815,7 @@ class FFI { } else { // Fetch the image buffer from rust codes. final sz = platformFFI.getRgbaSize(sessionId); - if (sz == null || sz == 0) { + if (sz == 0) { return; } final rgba = platformFFI.getRgba(sessionId, sz); diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index d2e11e2c1..80809309a 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -21,16 +21,8 @@ class RgbaFrame extends Struct { external Pointer data; } -typedef F2 = Pointer Function(Pointer, Pointer); typedef F3 = Pointer Function(Pointer); -typedef F4 = Uint64 Function(Pointer); -typedef F4Dart = int Function(Pointer); -typedef F5 = Void Function(Pointer); -typedef F5Dart = void Function(Pointer); typedef HandleEvent = Future Function(Map evt); -// pub fn session_register_texture(id: *const char, ptr: usize) -typedef F6 = Void Function(Pointer, Uint64); -typedef F6Dart = void Function(Pointer, int); /// FFI wrapper around the native Rust core. /// Hides the platform differences. @@ -38,7 +30,6 @@ class PlatformFFI { String _dir = ''; // _homeDir is only needed for Android and IOS. String _homeDir = ''; - F2? _translate; final _eventHandlers = >{}; late RustdeskImpl _ffiBind; late String _appType; @@ -51,9 +42,6 @@ class PlatformFFI { RustdeskImpl get ffiBind => _ffiBind; F3? _session_get_rgba; - F4Dart? _session_get_rgba_size; - F5Dart? _session_next_rgba; - F6Dart? _session_register_texture; static get localeName => Platform.localeName; @@ -89,18 +77,8 @@ class PlatformFFI { } } - String translate(String name, String locale) { - if (_translate == null) return name; - var a = name.toNativeUtf8(); - var b = locale.toNativeUtf8(); - var p = _translate!(a, b); - assert(p != nullptr); - final res = p.toDartString(); - calloc.free(p); - calloc.free(a); - calloc.free(b); - return res; - } + String translate(String name, String locale) => + _ffiBind.translate(name: name, locale: locale); Uint8List? getRgba(SessionID sessionId, int bufSize) { if (_session_get_rgba == null) return null; @@ -118,30 +96,11 @@ class PlatformFFI { } } - int? getRgbaSize(SessionID sessionId) { - if (_session_get_rgba_size == null) return null; - final sessionIdStr = sessionId.toString(); - var a = sessionIdStr.toNativeUtf8(); - final bufferSize = _session_get_rgba_size!(a); - malloc.free(a); - return bufferSize; - } - - void nextRgba(SessionID sessionId) { - if (_session_next_rgba == null) return; - final sessionIdStr = sessionId.toString(); - final a = sessionIdStr.toNativeUtf8(); - _session_next_rgba!(a); - malloc.free(a); - } - - void registerTexture(SessionID sessionId, int ptr) { - if (_session_register_texture == null) return; - final sessionIdStr = sessionId.toString(); - final a = sessionIdStr.toNativeUtf8(); - _session_register_texture!(a, ptr); - malloc.free(a); - } + int getRgbaSize(SessionID sessionId) => + _ffiBind.sessionGetRgbaSize(sessionId: sessionId); + void nextRgba(SessionID sessionId) => _ffiBind.sessionNextRgba(sessionId: sessionId); + void registerTexture(SessionID sessionId, int ptr) => + _ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr); /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { @@ -157,14 +116,7 @@ class PlatformFFI { : DynamicLibrary.process(); debugPrint('initializing FFI $_appType'); try { - _translate = dylib.lookupFunction('translate'); _session_get_rgba = dylib.lookupFunction("session_get_rgba"); - _session_get_rgba_size = - dylib.lookupFunction("session_get_rgba_size"); - _session_next_rgba = - dylib.lookupFunction("session_next_rgba"); - _session_register_texture = - dylib.lookupFunction("session_register_texture"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 9fb91f463..00c5490e0 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -40,8 +40,6 @@ class ServerModel with ChangeNotifier { late String _emptyIdShow; late final IDTextEditingController _serverId; - final _serverPasswd = - TextEditingController(text: translate("Generating ...")); final tabController = DesktopTabController(tabType: DesktopTabType.cm); @@ -63,6 +61,9 @@ class ServerModel with ChangeNotifier { int get connectStatus => _connectStatus; + TextEditingController get _serverPasswd => + TextEditingController(text: translate("Generating ...")); + String get verificationMethod { final index = [ kUseTemporaryPassword, diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index cfd41ab75..415f46eeb 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.17.1" colorize: dependency: transitive description: @@ -743,10 +743,10 @@ packages: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.18.0" io: dependency: transitive description: @@ -799,10 +799,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.2.0" meta: dependency: transitive description: @@ -1474,14 +1474,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - web: - dependency: transitive - description: - name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 - url: "https://pub.dev" - source: hosted - version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -1565,5 +1557,5 @@ packages: source: hosted version: "0.2.0" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.7.0-0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index b7141455d..691f86066 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.2.3 +version: 1.2.3+39 environment: sdk: ">=2.17.0" diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index f40ff1463..a48da5ff0 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -214,7 +214,7 @@ pub struct Resolution { pub h: i32, } -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct PeerConfig { #[serde(default, deserialize_with = "deserialize_vec_u8")] pub password: Vec, @@ -296,6 +296,38 @@ pub struct PeerConfig { pub transfer: TransferSerde, } +impl Default for PeerConfig { + fn default() -> Self { + Self { + password: Default::default(), + size: Default::default(), + size_ft: Default::default(), + size_pf: Default::default(), + view_style: Self::default_view_style(), + scroll_style: Self::default_scroll_style(), + image_quality: Self::default_image_quality(), + custom_image_quality: Self::default_custom_image_quality(), + show_remote_cursor: Default::default(), + lock_after_session_end: Default::default(), + privacy_mode: Default::default(), + allow_swap_key: Default::default(), + port_forwards: Default::default(), + direct_failures: Default::default(), + disable_audio: Default::default(), + disable_clipboard: Default::default(), + enable_file_transfer: Default::default(), + show_quality_monitor: Default::default(), + keyboard_mode: Default::default(), + view_only: Default::default(), + custom_resolutions: Default::default(), + options: Self::default_options(), + ui_flutter: Default::default(), + info: Default::default(), + transfer: Default::default(), + } + } +} + #[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] pub struct PeerInfoSerde { #[serde(default, deserialize_with = "deserialize_string")] @@ -1124,6 +1156,17 @@ impl PeerConfig { D: de::Deserializer<'de>, { let mut mp: HashMap = de::Deserialize::deserialize(deserializer)?; + Self::insert_default_options(&mut mp); + Ok(mp) + } + + fn default_options() -> HashMap { + let mut mp: HashMap = Default::default(); + Self::insert_default_options(&mut mp); + return mp; + } + + fn insert_default_options(mp: &mut HashMap) { let mut key = "codec-preference"; if !mp.contains_key(key) { mp.insert(key.to_owned(), UserDefaultConfig::read().get(key)); @@ -1136,7 +1179,10 @@ impl PeerConfig { if !mp.contains_key(key) { mp.insert(key.to_owned(), UserDefaultConfig::read().get(key)); } - Ok(mp) + key = "touch-mode"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::read().get(key)); + } } } diff --git a/src/flutter.rs b/src/flutter.rs index af9580587..f9434d98f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1037,24 +1037,24 @@ fn serialize_resolutions(resolutions: &Vec) -> String { } fn char_to_session_id(c: *const char) -> ResultType { + if c.is_null() { + bail!("Session id ptr is null"); + } let cstr = unsafe { std::ffi::CStr::from_ptr(c as _) }; let str = cstr.to_str()?; SessionID::from_str(str).map_err(|e| anyhow!("{:?}", e)) } -#[no_mangle] -pub fn session_get_rgba_size(_session_uuid_str: *const char) -> usize { +pub fn session_get_rgba_size(_session_id: SessionID) -> usize { #[cfg(not(feature = "flutter_texture_render"))] - if let Ok(session_id) = char_to_session_id(_session_uuid_str) { - if let Some(session) = SESSIONS.read().unwrap().get(&session_id) { - return session.rgba.read().unwrap().len(); - } + if let Some(session) = SESSIONS.read().unwrap().get(&_session_id) { + return session.rgba.read().unwrap().len(); } 0 } #[no_mangle] -pub fn session_get_rgba(session_uuid_str: *const char) -> *const u8 { +pub extern "C" fn session_get_rgba(session_uuid_str: *const char) -> *const u8 { if let Ok(session_id) = char_to_session_id(session_uuid_str) { if let Some(session) = SESSIONS.read().unwrap().get(&session_id) { return session.get_rgba(); @@ -1064,23 +1064,17 @@ pub fn session_get_rgba(session_uuid_str: *const char) -> *const u8 { std::ptr::null() } -#[no_mangle] -pub fn session_next_rgba(session_uuid_str: *const char) { - if let Ok(session_id) = char_to_session_id(session_uuid_str) { - if let Some(session) = SESSIONS.read().unwrap().get(&session_id) { - return session.next_rgba(); - } +pub fn session_next_rgba(session_id: SessionID) { + if let Some(session) = SESSIONS.read().unwrap().get(&session_id) { + return session.next_rgba(); } } #[inline] -#[no_mangle] -pub fn session_register_texture(_session_uuid_str: *const char, _ptr: usize) { +pub fn session_register_texture(_session_id: SessionID, _ptr: usize) { #[cfg(feature = "flutter_texture_render")] - if let Ok(session_id) = char_to_session_id(_session_uuid_str) { - if let Some(session) = SESSIONS.write().unwrap().get_mut(&session_id) { - return session.register_texture(_ptr); - } + if let Some(session) = SESSIONS.write().unwrap().get_mut(&_session_id) { + return session.register_texture(_ptr); } } @@ -1211,9 +1205,6 @@ pub fn session_send_pointer(session_id: SessionID, msg: String) { } } -#[no_mangle] -unsafe extern "C" fn get_rgba() {} - /// Hooks for session. #[derive(Clone)] pub enum SessionHook { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index fb6fea40b..eaf273d2e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1388,18 +1388,6 @@ pub fn main_get_build_date() -> String { crate::BUILD_DATE.to_string() } -#[no_mangle] -unsafe extern "C" fn translate(name: *const c_char, locale: *const c_char) -> *const c_char { - let name = CStr::from_ptr(name); - let locale = CStr::from_ptr(locale); - let res = if let (Ok(name), Ok(locale)) = (name.to_str(), locale.to_str()) { - crate::client::translate_locale(name.to_owned(), locale) - } else { - String::new() - }; - CString::from_vec_unchecked(res.into_bytes()).into_raw() -} - fn handle_query_onlines(onlines: Vec, offlines: Vec) { let data = HashMap::from([ ("name", "callback_query_onlines".to_owned()), @@ -1412,6 +1400,22 @@ fn handle_query_onlines(onlines: Vec, offlines: Vec) { ); } +pub fn translate(name: String, locale: String) -> SyncReturn { + SyncReturn(crate::client::translate_locale(name, &locale)) +} + +pub fn session_get_rgba_size(session_id: SessionID) -> SyncReturn { + SyncReturn(super::flutter::session_get_rgba_size(session_id)) +} + +pub fn session_next_rgba(session_id: SessionID) -> SyncReturn<()> { + SyncReturn(super::flutter::session_next_rgba(session_id)) +} + +pub fn session_register_texture(session_id: SessionID, ptr: usize) -> SyncReturn<()> { + SyncReturn(super::flutter::session_register_texture(session_id, ptr)) +} + pub fn query_onlines(ids: Vec) { #[cfg(not(any(target_os = "ios")))] crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines)