fix conflicts

This commit is contained in:
Asur4s
2023-01-12 00:35:39 +08:00
244 changed files with 8727 additions and 3173 deletions

1
flutter/.gitignore vendored
View File

@@ -54,3 +54,4 @@ lib/generated_bridge.freezed.dart
flutter_export_environment.sh
Flutter-Generated.xcconfig
key.jks
macos/rustdesk.xcodeproj/project.xcworkspace/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261 253" width="261" height="253"><path fill="#0ff" d="m1 217c0-5.5 4.5-10 10-10h60c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-60c-5.5 0-10-4.5-10-10z"/><path fill="#487997" d="m89 216c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#c0ff00" d="m3 166c0-5.5 4.5-10 10-10h34c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-34c-5.5 0-10-4.5-10-10z"/><path fill="#00f" d="m63 166c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m140 215c0-5.5 4.5-10 10-10h26c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-26c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m190 215c0-5.5 4.5-10 10-10h26c5.5 0 10 4.5 10 10v28c0 5.5-4.5 10-10 10h-26c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m112 166c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m165 165c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m216 164c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m0 115c0-5.5 4.5-10 10-10h61c5.5 0 10 4.5 10 10v23c0 5.5-4.5 10-10 10h-61c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m90 116c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m139 116c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m191 115c0-5.5 4.5-10 10-10h23c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-23c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m2 62c0-5.5 4.5-10 10-10h50c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-50c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m79 62c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m131 64c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m182 63c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m0 10c0-5.5 4.5-10 10-10h28c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-28c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m54 11c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m105 12c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m156 13c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path d="m16.7 171.9v1.9q-1.1-0.5-2.1-0.8-1-0.2-1.9-0.2-1.7 0-2.5 0.6-0.9 0.6-0.9 1.8 0 0.9 0.6 1.4 0.6 0.5 2.2 0.8l1.2 0.3q2.2 0.4 3.2 1.4 1.1 1.1 1.1 2.9 0 2.1-1.4 3.2-1.5 1.1-4.2 1.1-1 0-2.2-0.3-1.2-0.2-2.4-0.6v-2.1q1.2 0.7 2.3 1 1.2 0.4 2.3 0.4 1.7 0 2.6-0.7 0.9-0.6 0.9-1.9 0-1.1-0.6-1.7-0.7-0.6-2.2-0.9l-1.2-0.2q-2.2-0.4-3.2-1.4-1-0.9-1-2.6 0-1.9 1.4-3 1.3-1.1 3.7-1.1 1.1 0 2.1 0.1 1.1 0.2 2.2 0.6zm13 7.5v6.6h-1.8v-6.5q0-1.6-0.6-2.4-0.6-0.7-1.8-0.7-1.5 0-2.3 0.9-0.9 0.9-0.9 2.5v6.2h-1.8v-15.2h1.8v6q0.7-1 1.5-1.5 0.9-0.5 2.1-0.5 1.8 0 2.8 1.2 1 1.1 1 3.4zm3.6 6.6v-10.9h1.8v10.9zm0-12.9v-2.3h1.7v2.3zm9.4-2.3h1.7v1.5h-1.7q-0.9 0-1.3 0.4-0.4 0.4-0.4 1.4v1h3v1.4h-3v9.5h-1.8v-9.5h-1.7v-1.4h1.7v-0.8q0-1.8 0.8-2.7 0.9-0.8 2.7-0.8zm2.9 1.2h1.8v3.1h3.7v1.4h-3.7v5.9q0 1.3 0.3 1.7 0.4 0.4 1.5 0.4h1.9v1.5h-1.9q-2.1 0-2.8-0.8-0.8-0.8-0.8-2.8v-5.9h-1.4v-1.4h1.4z"/></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 253" width="260" height="253"><path fill="#c0ff00" d="m0 167c0-5.5 4.5-10 10-10h90c5.5 0 10 4.5 10 10v23c0 5.5-4.5 10-10 10h-90c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m3 63c0-5.5 4.5-10 10-10h46c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-46c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m1 114c0-5.5 4.5-10 10-10h62c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-62c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m114 166c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v24c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m90 117c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v22c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m81 64c0-5.5 4.5-10 10-10h22c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-22c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m54 10c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m2 10c0-5.5 4.5-10 10-10h26c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-26c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m2 216c0-5.5 4.5-10 10-10h59c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-59c-5.5 0-10-4.5-10-10z"/><path fill="#487997" d="m89 217c0-5.5 4.5-10 10-10h23c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-23c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m141 217c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#0ff" d="m191 216c0-5.5 4.5-10 10-10h23c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-23c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m166 164c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v27c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m215 165c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m138 111c0-5.5 4.5-10 10-10h28c5.5 0 10 4.5 10 10v29c0 5.5-4.5 10-10 10h-28c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m192 112c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v29c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m129 64c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m182 62c0-5.5 4.5-10 10-10h25c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-25c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m105 11c0-5.5 4.5-10 10-10h24c5.5 0 10 4.5 10 10v26c0 5.5-4.5 10-10 10h-24c-5.5 0-10-4.5-10-10z"/><path fill="#00ffa8" d="m154 12c0-5.5 4.5-10 10-10h27c5.5 0 10 4.5 10 10v25c0 5.5-4.5 10-10 10h-27c-5.5 0-10-4.5-10-10z"/><path d="m42.7 172.9v1.9q-1.1-0.5-2.1-0.8-1-0.2-1.9-0.2-1.7 0-2.5 0.6-0.9 0.6-0.9 1.8 0 0.9 0.6 1.4 0.6 0.5 2.2 0.8l1.2 0.3q2.2 0.4 3.2 1.4 1.1 1.1 1.1 2.9 0 2.1-1.4 3.2-1.5 1.1-4.2 1.1-1 0-2.2-0.3-1.2-0.2-2.4-0.6v-2.1q1.2 0.7 2.3 1 1.2 0.4 2.3 0.4 1.7 0 2.6-0.7 0.9-0.6 0.9-1.9 0-1.1-0.6-1.7-0.7-0.6-2.2-0.9l-1.2-0.2q-2.2-0.4-3.2-1.4-1-0.9-1-2.6 0-1.9 1.4-3 1.3-1.1 3.7-1.1 1.1 0 2.1 0.1 1.1 0.2 2.2 0.6zm13 7.5v6.6h-1.8v-6.5q0-1.6-0.6-2.4-0.6-0.7-1.8-0.7-1.5 0-2.3 0.9-0.9 0.9-0.9 2.5v6.2h-1.8v-15.2h1.8v6q0.7-1 1.5-1.5 0.9-0.5 2.1-0.5 1.8 0 2.8 1.2 1 1.1 1 3.4zm3.6 6.6v-10.9h1.8v10.9zm0-12.9v-2.3h1.7v2.3zm9.4-2.3h1.7v1.5h-1.7q-0.9 0-1.3 0.4-0.4 0.4-0.4 1.4v1h3v1.4h-3v9.5h-1.8v-9.5h-1.7v-1.4h1.7v-0.8q0-1.8 0.8-2.7 0.9-0.8 2.7-0.8zm2.9 1.2h1.8v3.1h3.7v1.4h-3.7v5.9q0 1.3 0.3 1.7 0.4 0.4 1.5 0.4h1.9v1.5h-1.9q-2.1 0-2.8-0.8-0.8-0.8-0.8-2.8v-5.9h-1.4v-1.4h1.4z"/></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# Build libyuv / opus / libvpx / oboe for Android
# Required:
# Build libyuv / opus / libvpx / oboe for Android
# Required:
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables
# 2. vcpkg initialized
# 3. ndk, version: 22 (if ndk < 22 you need to change LD as `export LD=$TOOLCHAIN/bin/$NDK_LLVM_TARGET-ld`)
@@ -23,7 +23,7 @@ HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/l
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG
function build {
ANDROID_ABI=$1
ANDROID_ABI=$1
VCPKG_TARGET=$2
NDK_LLVM_TARGET=$3
LIBVPX_TARGET=$4
@@ -111,15 +111,15 @@ patch -N -d build/oboe -p1 < ../src/oboe.patch
# x86_64-linux-android
# i686-linux-android
# LIBVPX_TARGET :
# arm64-android-gcc
# armv7-android-gcc
# LIBVPX_TARGET :
# arm64-android-gcc
# armv7-android-gcc
# x86_64-android-gcc
# x86-android-gcc
# x86-android-gcc
# args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET
build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc
build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc
build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc
# rm -rf build/libvpx
# rm -rf build/oboe

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 811 B

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 B

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 997 B

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 B

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -46,7 +46,7 @@ var isWebDesktop = false;
var version = "";
int androidVersion = 0;
/// only avaliable for Windows target
/// only available for Windows target
int windowsBuildNumber = 0;
DesktopType? desktopType;
@@ -99,22 +99,28 @@ class IconFont {
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
const ColorThemeExtension({
required this.border,
required this.highlight,
});
final Color? border;
final Color? highlight;
static const light = ColorThemeExtension(
border: Color(0xFFCCCCCC),
highlight: Color(0xFFE5E5E5),
);
static const dark = ColorThemeExtension(
border: Color(0xFF555555),
highlight: Color(0xFF3F3F3F),
);
@override
ThemeExtension<ColorThemeExtension> copyWith({Color? border}) {
ThemeExtension<ColorThemeExtension> copyWith(
{Color? border, Color? highlight}) {
return ColorThemeExtension(
border: border ?? this.border,
highlight: highlight ?? this.highlight,
);
}
@@ -126,6 +132,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
}
return ColorThemeExtension(
border: Color.lerp(border, other.border, t),
highlight: Color.lerp(highlight, other.highlight, t),
);
}
}
@@ -223,7 +230,7 @@ class MyTheme {
bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString());
}
bind.mainChangeTheme(dark: currentThemeMode().toShortString());
bind.mainChangeTheme(dark: mode.toShortString());
}
}
@@ -970,11 +977,13 @@ Future<bool> matchPeer(String searchText, Peer peer) async {
/// Get the image for the current [platform].
Widget getPlatformImage(String platform, {double size = 50}) {
platform = platform.toLowerCase();
if (platform == 'mac os') {
if (platform == kPeerPlatformMacOS) {
platform = 'mac';
} else if (platform != 'linux' && platform != 'android') {
} else if (platform != kPeerPlatformLinux &&
platform != kPeerPlatformAndroid) {
platform = 'win';
} else {
platform = platform.toLowerCase();
}
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
}
@@ -1360,13 +1369,13 @@ connect(BuildContext context, String id,
}
}
Future<Map<String, String>> getHttpHeaders() async {
Map<String, String> getHttpHeaders() {
return {
'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
};
}
// Simple wrapper of built-in types for refrence use.
// Simple wrapper of built-in types for reference use.
class SimpleWrapper<T> {
T value;
SimpleWrapper(this.value);
@@ -1402,7 +1411,7 @@ Future<void> reloadAllWindows() async {
/// Indicate the flutter app is running in portable mode.
///
/// [Note]
/// Portable build is only avaliable on Windows.
/// Portable build is only available on Windows.
bool isRunningInPortableMode() {
if (!Platform.isWindows) {
return false;
@@ -1411,7 +1420,7 @@ bool isRunningInPortableMode() {
}
/// Window status callback
void onActiveWindowChanged() async {
Future<void> onActiveWindowChanged() async {
print(
"[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}");
if (rustDeskWinManager.getActiveWindows().isEmpty) {
@@ -1503,3 +1512,53 @@ Pointer<win32.OSVERSIONINFOEX> getOSVERSIONINFOEXPointer() {
bool get kUseCompatibleUiMode =>
Platform.isWindows &&
const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
class ServerConfig {
late String idServer;
late String relayServer;
late String apiServer;
late String key;
ServerConfig(
{String? idServer, String? relayServer, String? apiServer, String? key}) {
this.idServer = idServer?.trim() ?? '';
this.relayServer = relayServer?.trim() ?? '';
this.apiServer = apiServer?.trim() ?? '';
this.key = key?.trim() ?? '';
}
/// decode from shared string (from user shared or rustdesk-server generated)
/// also see [encode]
/// throw when decoding failure
ServerConfig.decode(String msg) {
final input = msg.split('').reversed.join('');
final bytes = base64Decode(base64.normalize(input));
final json = jsonDecode(utf8.decode(bytes));
idServer = json['host'] ?? '';
relayServer = json['relay'] ?? '';
apiServer = json['api'] ?? '';
key = json['key'] ?? '';
}
/// encode to shared string
/// also see [ServerConfig.decode]
String encode() {
Map<String, String> config = {};
config['host'] = idServer.trim();
config['relay'] = relayServer.trim();
config['api'] = apiServer.trim();
config['key'] = key.trim();
return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits))
.split('')
.reversed
.join();
}
/// from local options
ServerConfig.fromOptions(Map<String, dynamic> options)
: idServer = options['custom-rendezvous-server'] ?? "",
relayServer = options['relay-server'] ?? "",
apiServer = options['api-server'] ?? "",
key = options['key'] ?? "";
}

View File

@@ -0,0 +1,119 @@
import 'package:flutter_hbb/models/peer_model.dart';
class HttpType {
static const kAuthReqTypeAccount = "account";
static const kAuthReqTypeMobile = "mobile";
static const kAuthReqTypeSMSCode = "sms_code";
static const kAuthReqTypeEmailCode = "email_code";
static const kAuthResTypeToken = "access_token";
static const kAuthResTypeEmailCheck = "email_check";
}
class UserPayload {
String name = '';
String email = '';
String note = '';
int? status;
String grp = '';
bool isAdmin = false;
UserPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '',
email = json['email'] ?? '',
note = json['note'] ?? '',
status = json['status'],
grp = json['grp'] ?? '',
isAdmin = json['is_admin'] == true;
}
class PeerPayload {
String id = '';
String info = '';
int? status;
String user = '';
String user_name = '';
String note = '';
PeerPayload.fromJson(Map<String, dynamic> json)
: id = json['id'] ?? '',
info = json['info'] ?? '',
status = json['status'],
user = json['user'] ?? '',
user_name = json['user_name'] ?? '',
note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) {
return Peer.fromJson({"id": p.id});
}
}
class LoginRequest {
String? username;
String? password;
String? id;
String? uuid;
bool? autoLogin;
String? type;
String? verificationCode;
String? deviceInfo;
LoginRequest(
{this.username,
this.password,
this.id,
this.uuid,
this.autoLogin,
this.type,
this.verificationCode,
this.deviceInfo});
LoginRequest.fromJson(Map<String, dynamic> json) {
username = json['username'];
password = json['password'];
id = json['id'];
uuid = json['uuid'];
autoLogin = json['autoLogin'];
type = json['type'];
verificationCode = json['verificationCode'];
deviceInfo = json['deviceInfo'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['username'] = username ?? '';
data['password'] = password ?? '';
data['id'] = id ?? '';
data['uuid'] = uuid ?? '';
data['autoLogin'] = autoLogin ?? '';
data['type'] = type ?? '';
data['verificationCode'] = verificationCode ?? '';
data['deviceInfo'] = deviceInfo ?? '';
return data;
}
}
class LoginResponse {
String? access_token;
String? type;
UserPayload? user;
LoginResponse({this.access_token, this.type, this.user});
LoginResponse.fromJson(Map<String, dynamic> json) {
access_token = json['access_token'];
type = json['type'];
user = json['user'] != null ? UserPayload.fromJson(json['user']) : null;
}
}
class RequestException implements Exception {
int statusCode;
String cause;
RequestException(this.statusCode, this.cause);
@override
String toString() {
return "RequestException, statusCode: $statusCode, error: $cause";
}
}

View File

@@ -3,14 +3,12 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/login.dart';
import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart';
import '../../common.dart';
import '../../desktop/pages/desktop_home_page.dart';
import '../../mobile/pages/settings_page.dart';
import 'login.dart';
class AddressBook extends StatefulWidget {
final EdgeInsets? menuPadding;
@@ -28,7 +26,6 @@ class _AddressBookState extends State<AddressBook> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb());
}
@override
@@ -42,25 +39,12 @@ class _AddressBookState extends State<AddressBook> {
}
});
handleLogin() {
// TODO refactor login dialog for desktop and mobile
if (isDesktop) {
loginDialog().then((success) {
if (success) {
gFFI.abModel.pullAb();
}
});
} else {
showLogin(gFFI.dialogManager);
}
}
Future<Widget> buildBody(BuildContext context) async {
return Obx(() {
if (gFFI.userModel.userName.value.isEmpty) {
return Center(
child: InkWell(
onTap: handleLogin,
onTap: loginDialog,
child: Text(
translate("Login"),
style: const TextStyle(decoration: TextDecoration.underline),

View File

@@ -0,0 +1,676 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
class _IconOP extends StatelessWidget {
final String icon;
final double iconWidth;
final EdgeInsets margin;
const _IconOP(
{Key? key,
required this.icon,
required this.iconWidth,
this.margin = const EdgeInsets.symmetric(horizontal: 4.0)})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: margin,
child: SvgPicture.asset(
'assets/$icon.svg',
width: iconWidth,
),
);
}
}
class ButtonOP extends StatelessWidget {
final String op;
final RxString curOP;
final double iconWidth;
final Color primaryColor;
final double height;
final Function() onTap;
const ButtonOP({
Key? key,
required this.op,
required this.curOP,
required this.iconWidth,
required this.primaryColor,
required this.height,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(children: [
Container(
height: height,
width: 200,
child: Obx(() => ElevatedButton(
style: ElevatedButton.styleFrom(
primary: curOP.value.isEmpty || curOP.value == op
? primaryColor
: Colors.grey,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null,
child: Row(
children: [
SizedBox(
width: 30,
child: _IconOP(
icon: op,
iconWidth: iconWidth,
margin: EdgeInsets.only(right: 5),
)),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Center(
child: Text('${translate("Continue with")} $op')))),
],
))),
),
]);
}
}
class ConfigOP {
final String op;
final double iconWidth;
ConfigOP({required this.op, required this.iconWidth});
}
class WidgetOP extends StatefulWidget {
final ConfigOP config;
final RxString curOP;
final Function(String) cbLogin;
const WidgetOP({
Key? key,
required this.config,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _WidgetOPState();
}
}
class _WidgetOPState extends State<WidgetOP> {
Timer? _updateTimer;
String _stateMsg = '';
String _failedMsg = '';
String _url = '';
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
_updateTimer?.cancel();
}
_beginQueryState() {
_updateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
_updateState();
});
}
_updateState() {
bind.mainAccountAuthResult().then((result) {
if (result.isEmpty) {
return;
}
final resultMap = jsonDecode(result);
if (resultMap == null) {
return;
}
final String stateMsg = resultMap['state_msg'];
String failedMsg = resultMap['failed_msg'];
final String? url = resultMap['url'];
final authBody = resultMap['auth_body'];
if (_stateMsg != stateMsg || _failedMsg != failedMsg) {
if (_url.isEmpty && url != null && url.isNotEmpty) {
launchUrl(Uri.parse(url));
_url = url;
}
if (authBody != null) {
_updateTimer?.cancel();
final String username = authBody['user']['name'];
widget.curOP.value = '';
widget.cbLogin(username);
}
setState(() {
_stateMsg = stateMsg;
_failedMsg = failedMsg;
if (failedMsg.isNotEmpty) {
widget.curOP.value = '';
_updateTimer?.cancel();
}
});
}
});
}
_resetState() {
_stateMsg = '';
_failedMsg = '';
_url = '';
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ButtonOP(
op: widget.config.op,
curOP: widget.curOP,
iconWidth: widget.config.iconWidth,
primaryColor: str2color(widget.config.op, 0x7f),
height: 36,
onTap: () async {
_resetState();
widget.curOP.value = widget.config.op;
await bind.mainAccountAuth(op: widget.config.op);
_beginQueryState();
},
),
Obx(() {
if (widget.curOP.isNotEmpty &&
widget.curOP.value != widget.config.op) {
_failedMsg = '';
}
return Offstage(
offstage:
_failedMsg.isEmpty && widget.curOP.value != widget.config.op,
child: Row(
children: [
Text(
_stateMsg,
style: TextStyle(fontSize: 12),
),
SizedBox(width: 8),
Text(
_failedMsg,
style: TextStyle(
fontSize: 14,
color: Colors.red,
),
),
],
));
}),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: const SizedBox(
height: 5.0,
),
),
),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 20),
child: ElevatedButton(
onPressed: () {
widget.curOP.value = '';
_updateTimer?.cancel();
_resetState();
bind.mainAccountAuthCancel();
},
child: Text(
translate('Cancel'),
style: TextStyle(fontSize: 15),
),
),
),
),
),
],
);
}
}
class LoginWidgetOP extends StatelessWidget {
final List<ConfigOP> ops;
final RxString curOP;
final Function(String) cbLogin;
LoginWidgetOP({
Key? key,
required this.ops,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var children = ops
.map((op) => [
WidgetOP(
config: op,
curOP: curOP,
cbLogin: cbLogin,
),
const Divider(
indent: 5,
endIndent: 5,
)
])
.expand((i) => i)
.toList();
if (children.isNotEmpty) {
children.removeLast();
}
return SingleChildScrollView(
child: Container(
width: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
)));
}
}
class LoginWidgetUserPass extends StatelessWidget {
final TextEditingController username;
final TextEditingController pass;
final String? usernameMsg;
final String? passMsg;
final bool isInProgress;
final RxString curOP;
final RxBool autoLogin;
final Function() onLogin;
final FocusNode? userFocusNode;
const LoginWidgetUserPass({
Key? key,
this.userFocusNode,
required this.username,
required this.pass,
required this.usernameMsg,
required this.passMsg,
required this.isInProgress,
required this.curOP,
required this.autoLogin,
required this.onLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 8.0),
DialogTextField(
title: '${translate("Username")}:',
controller: username,
focusNode: userFocusNode,
prefixIcon: Icon(Icons.account_circle_outlined),
errorText: usernameMsg),
DialogTextField(
title: '${translate("Password")}:',
obscureText: true,
controller: pass,
prefixIcon: Icon(Icons.lock_outline),
errorText: passMsg),
Obx(() => CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(
translate("Remember me"),
),
value: autoLogin.value,
onChanged: (v) {
if (v == null) return;
autoLogin.value = v;
},
)),
Offstage(
offstage: !isInProgress,
child: const LinearProgressIndicator()),
const SizedBox(height: 12.0),
FittedBox(
child:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Container(
height: 38,
width: 200,
child: Obx(() => ElevatedButton(
child: Text(
translate('Login'),
style: TextStyle(fontSize: 16),
),
onPressed:
curOP.value.isEmpty || curOP.value == 'rustdesk'
? () {
onLogin();
}
: null,
)),
),
])),
],
));
}
}
class DialogTextField extends StatelessWidget {
final String title;
final bool obscureText;
final String? errorText;
final String? helperText;
final Widget? prefixIcon;
final TextEditingController controller;
final FocusNode? focusNode;
DialogTextField(
{Key? key,
this.focusNode,
this.obscureText = false,
this.errorText,
this.helperText,
this.prefixIcon,
required this.title,
required this.controller})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: title,
border: const OutlineInputBorder(),
prefixIcon: prefixIcon,
helperText: helperText,
helperMaxLines: 8,
errorText: errorText),
controller: controller,
focusNode: focusNode,
autofocus: true,
obscureText: obscureText,
),
),
],
).paddingSymmetric(vertical: 4.0);
}
}
/// common login dialog for desktop
/// call this directly
Future<bool?> loginDialog() async {
var username = TextEditingController();
var password = TextEditingController();
final userFocusNode = FocusNode()..requestFocus();
Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus());
String? usernameMsg;
String? passwordMsg;
var isInProgress = false;
final autoLogin = true.obs;
final RxString curOP = ''.obs;
final res = await gFFI.dialogManager.show<bool>((setState, close) {
username.addListener(() {
if (usernameMsg != null) {
setState(() => usernameMsg = null);
}
});
password.addListener(() {
if (passwordMsg != null) {
setState(() => passwordMsg = null);
}
});
onDialogCancel() {
isInProgress = false;
close(false);
}
onLogin() async {
// validate
if (username.text.isEmpty) {
setState(() => usernameMsg = translate('Username missed'));
return;
}
if (password.text.isEmpty) {
setState(() => passwordMsg = translate('Password missed'));
return;
}
curOP.value = 'rustdesk';
setState(() => isInProgress = true);
try {
final resp = await gFFI.userModel.login(LoginRequest(
username: username.text,
password: password.text,
id: await bind.mainGetMyId(),
uuid: await bind.mainGetUuid(),
autoLogin: autoLogin.value,
type: HttpType.kAuthReqTypeAccount));
switch (resp.type) {
case HttpType.kAuthResTypeToken:
if (resp.access_token != null) {
await bind.mainSetLocalOption(
key: 'access_token', value: resp.access_token!);
close(true);
return;
}
break;
case HttpType.kAuthResTypeEmailCheck:
setState(() => isInProgress = false);
final res = await verificationCodeDialog(resp.user);
if (res == true) {
close(true);
return;
}
break;
default:
passwordMsg = "Failed, bad response from server";
break;
}
} on RequestException catch (err) {
passwordMsg = translate(err.cause);
debugPrintStack(label: err.toString());
} catch (err) {
passwordMsg = "Unknown Error: $err";
debugPrintStack(label: err.toString());
}
curOP.value = '';
setState(() => isInProgress = false);
}
return CustomAlertDialog(
title: Text(translate('Login')),
contentBoxConstraints: BoxConstraints(minWidth: 400),
content: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
height: 8.0,
),
LoginWidgetUserPass(
username: username,
pass: password,
usernameMsg: usernameMsg,
passMsg: passwordMsg,
isInProgress: isInProgress,
curOP: curOP,
autoLogin: autoLogin,
onLogin: onLogin,
userFocusNode: userFocusNode,
),
const SizedBox(
height: 8.0,
),
Center(
child: Text(
translate('or'),
style: TextStyle(fontSize: 16),
)),
const SizedBox(
height: 8.0,
),
LoginWidgetOP(
ops: [
ConfigOP(op: 'GitHub', iconWidth: 20),
ConfigOP(op: 'Google', iconWidth: 20),
ConfigOP(op: 'Okta', iconWidth: 38),
],
curOP: curOP,
cbLogin: (String username) {
gFFI.userModel.userName.value = username;
close(true);
},
),
],
),
actions: [msgBoxButton(translate('Close'), onDialogCancel)],
onCancel: onDialogCancel,
);
});
if (res != null) {
// update ab and group status
await gFFI.abModel.pullAb();
await gFFI.groupModel.pull();
}
return res;
}
Future<bool?> verificationCodeDialog(UserPayload? user) async {
var autoLogin = true;
var isInProgress = false;
String? errorText;
final code = TextEditingController();
final focusNode = FocusNode()..requestFocus();
Timer(Duration(milliseconds: 100), () => focusNode..requestFocus());
final res = await gFFI.dialogManager.show<bool>((setState, close) {
bool validate() {
return code.text.length >= 6;
}
code.addListener(() {
if (errorText != null) {
setState(() => errorText = null);
}
});
void onVerify() async {
if (!validate()) {
setState(
() => errorText = translate('Too short, at least 6 characters.'));
return;
}
setState(() => isInProgress = true);
try {
final resp = await gFFI.userModel.login(LoginRequest(
verificationCode: code.text,
username: user?.name,
id: await bind.mainGetMyId(),
uuid: await bind.mainGetUuid(),
autoLogin: autoLogin,
type: HttpType.kAuthReqTypeEmailCode));
switch (resp.type) {
case HttpType.kAuthResTypeToken:
if (resp.access_token != null) {
await bind.mainSetLocalOption(
key: 'access_token', value: resp.access_token!);
close(true);
return;
}
break;
default:
errorText = "Failed, bad response from server";
break;
}
} on RequestException catch (err) {
errorText = translate(err.cause);
debugPrintStack(label: err.toString());
} catch (err) {
errorText = "Unknown Error: $err";
debugPrintStack(label: err.toString());
}
setState(() => isInProgress = false);
}
return CustomAlertDialog(
title: Text(translate("Verification code")),
contentBoxConstraints: BoxConstraints(maxWidth: 300),
content: Column(
children: [
Offstage(
offstage: user?.email == null,
child: TextField(
decoration: InputDecoration(
labelText: "Email",
prefixIcon: Icon(Icons.email),
border: InputBorder.none),
readOnly: true,
controller: TextEditingController(text: user?.email),
)),
const SizedBox(height: 8),
DialogTextField(
title: '${translate("Verification code")}:',
controller: code,
errorText: errorText,
focusNode: focusNode,
helperText: translate('verification_tip'),
),
CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Row(children: [
Expanded(child: Text(translate("Trust this device")))
]),
value: autoLogin,
onChanged: (v) {
if (v == null) return;
setState(() => autoLogin = !autoLogin);
},
),
Offstage(
offstage: !isInProgress,
child: const LinearProgressIndicator()),
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: onVerify, child: Text(translate("Verify"))),
]);
});
return res;
}

View File

@@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:get/get.dart';
import '../../common.dart';
class MyGroup extends StatefulWidget {
final EdgeInsets? menuPadding;
const MyGroup({Key? key, this.menuPadding}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _MyGroupState();
}
}
class _MyGroupState extends State<MyGroup> {
static final RxString selectedUser = ''.obs;
static final RxString searchUserText = ''.obs;
static TextEditingController searchUserController = TextEditingController();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) => FutureBuilder<Widget>(
future: buildBody(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data!;
} else {
return const Offstage();
}
});
Future<Widget> buildBody(BuildContext context) async {
return Obx(() {
if (gFFI.groupModel.userLoading.value) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (gFFI.groupModel.userLoadError.isNotEmpty) {
return _buildShowError(gFFI.groupModel.userLoadError.value);
}
if (isDesktop) {
return _buildDesktop();
} else {
return _buildMobile();
}
});
}
Widget _buildShowError(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(translate(error)),
TextButton(
onPressed: () {
gFFI.groupModel.pull();
},
child: Text(translate("Retry")))
],
));
}
Widget _buildDesktop() {
return Obx(
() => Row(
children: [
Card(
margin: EdgeInsets.symmetric(horizontal: 4.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Theme.of(context).scaffoldBackgroundColor)),
child: Container(
width: 200,
height: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Column(
children: [
_buildLeftHeader(),
Expanded(
child: Container(
width: double.infinity,
height: double.infinity,
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(2)),
child: _buildUserContacts(),
).marginSymmetric(vertical: 8.0),
)
],
),
),
).marginOnly(right: 8.0),
Expanded(
child: Align(
alignment: Alignment.topLeft,
child: MyGroupPeerView(
menuPadding: widget.menuPadding,
initPeers: gFFI.groupModel.peersShow.value)),
)
],
),
);
}
Widget _buildMobile() {
return Obx(
() => Column(
children: [
Card(
margin: EdgeInsets.symmetric(horizontal: 4.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Theme.of(context).scaffoldBackgroundColor)),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildLeftHeader(),
Container(
width: double.infinity,
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(4)),
child: _buildUserContacts(),
).marginSymmetric(vertical: 8.0)
],
),
),
),
Divider(),
Expanded(
child: Align(
alignment: Alignment.topLeft,
child: MyGroupPeerView(
menuPadding: widget.menuPadding,
initPeers: gFFI.groupModel.peersShow.value)),
)
],
),
);
}
Widget _buildLeftHeader() {
return Row(
children: [
Expanded(
child: TextField(
controller: searchUserController,
onChanged: (value) {
searchUserText.value = value;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.search_rounded,
color: Theme.of(context).hintColor,
),
contentPadding: const EdgeInsets.symmetric(vertical: 10),
hintText: translate("Search"),
hintStyle:
TextStyle(fontSize: 14, color: Theme.of(context).hintColor),
border: InputBorder.none,
isDense: true,
),
)),
],
);
}
Widget _buildUserContacts() {
return Obx(() {
return Column(
children: gFFI.groupModel.users
.where((p0) {
if (searchUserText.isNotEmpty) {
return p0.name.contains(searchUserText.value);
}
return true;
})
.map((e) => _buildUserItem(e.name))
.toList());
});
}
Widget _buildUserItem(String username) {
return InkWell(onTap: () {
if (selectedUser.value != username) {
selectedUser.value = username;
gFFI.groupModel.pullUserPeers(username);
}
}, child: Obx(
() {
bool selected = selectedUser.value == username;
return Container(
decoration: BoxDecoration(
color: selected ? MyTheme.color(context).highlight : null,
border: Border(
bottom: BorderSide(
width: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1))),
),
child: Container(
child: Row(
children: [
Icon(Icons.person_outline_rounded, color: Colors.grey, size: 16)
.marginOnly(right: 4),
Expanded(child: Text(username)),
],
).paddingSymmetric(vertical: 4),
),
);
},
)).marginSymmetric(horizontal: 12);
}
}

View File

@@ -321,6 +321,7 @@ enum CardType {
fav,
lan,
ab,
grp,
}
abstract class BasePeerCard extends StatelessWidget {
@@ -463,7 +464,7 @@ abstract class BasePeerCard extends StatelessWidget {
);
}
/// Only avaliable on Windows.
/// Only available on Windows.
@protected
MenuEntryBase<String> _createShortCutAction(String id) {
return MenuEntryButton<String>(
@@ -684,6 +685,9 @@ abstract class BasePeerCard extends StatelessWidget {
case CardType.ab:
gFFI.abModel.pullAb();
break;
case CardType.grp:
gFFI.groupModel.pull();
break;
}
}
}
@@ -937,6 +941,41 @@ class AddressBookPeerCard extends BasePeerCard {
}
}
class MyGroupPeerCard extends BasePeerCard {
MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
: super(
peer: peer,
cardType: CardType.grp,
menuPadding: menuPadding,
key: key);
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(_wolAction(peer.id));
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
return menuItems;
}
}
void _rdpDialog(String id, CardType card) async {
String port, username;
if (card == CardType.ab) {

View File

@@ -1,9 +1,9 @@
import 'dart:convert';
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/consts.dart';
@@ -16,6 +16,101 @@ import 'package:get/get.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
const int groupTabIndex = 4;
const String defaultGroupTabname = 'Group';
class StatePeerTab {
final RxInt currentTab = 0.obs;
final RxInt tabHiddenFlag = 0.obs;
final RxList<String> tabNames = [
'Recent Sessions',
'Favorites',
'Discovered',
'Address Book',
defaultGroupTabname,
].obs;
StatePeerTab._() {
tabHiddenFlag.value = (int.tryParse(
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
radix: 2) ??
0);
var tabs = _notHiddenTabs();
currentTab.value =
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
if (!tabs.contains(currentTab.value)) {
currentTab.value = 0;
}
}
static final StatePeerTab instance = StatePeerTab._();
check() {
var tabs = _notHiddenTabs();
if (filterGroupCard()) {
if (currentTab.value == groupTabIndex) {
currentTab.value =
tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0;
bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: currentTab.value.toString());
}
} else {
if (gFFI.userModel.isAdmin.isFalse &&
gFFI.userModel.groupName.isNotEmpty) {
tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
} else {
tabNames[groupTabIndex] = defaultGroupTabname;
}
if (tabs.contains(groupTabIndex) &&
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
groupTabIndex) {
currentTab.value = groupTabIndex;
}
}
}
List<int> currentTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i) && !_isTabFilter(i)) {
v.add(i);
}
}
return v;
}
bool filterGroupCard() {
if (gFFI.groupModel.users.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
return true;
} else {
return false;
}
}
bool _isTabHidden(int tabindex) {
return tabHiddenFlag & (1 << tabindex) != 0;
}
bool _isTabFilter(int tabIndex) {
if (tabIndex == groupTabIndex) {
return filterGroupCard();
}
return false;
}
List<int> _notHiddenTabs() {
var v = List<int>.empty(growable: true);
for (int i = 0; i < tabNames.length; i++) {
if (!_isTabHidden(i)) {
v.add(i);
}
}
return v;
}
}
final statePeerTab = StatePeerTab.instance;
class PeerTabPage extends StatefulWidget {
const PeerTabPage({Key? key}) : super(key: key);
@override
@@ -23,10 +118,9 @@ class PeerTabPage extends StatefulWidget {
}
class _TabEntry {
final String name;
final Widget widget;
final Function() load;
_TabEntry(this.name, this.widget, this.load);
_TabEntry(this.widget, this.load);
}
EdgeInsets? _menuPadding() {
@@ -35,65 +129,36 @@ EdgeInsets? _menuPadding() {
class _PeerTabPageState extends State<PeerTabPage>
with SingleTickerProviderStateMixin {
late final RxInt tabHiddenFlag;
late final RxString currentTab;
late final RxList<String> visibleOrderedTabs;
final List<_TabEntry> entries = [
_TabEntry(
'Recent Sessions',
RecentPeersView(
menuPadding: _menuPadding(),
),
bind.mainLoadRecentPeers),
_TabEntry(
'Favorites',
FavoritePeersView(
menuPadding: _menuPadding(),
),
bind.mainLoadFavPeers),
_TabEntry(
'Discovered',
DiscoveredPeersView(
menuPadding: _menuPadding(),
),
bind.mainDiscover),
_TabEntry(
'Address Book',
AddressBook(
menuPadding: _menuPadding(),
),
() => {}),
_TabEntry(
MyGroup(
menuPadding: _menuPadding(),
),
() => {}),
];
@override
void initState() {
tabHiddenFlag = (int.tryParse(
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
radix: 2) ??
0)
.obs;
currentTab = bind.getLocalFlutterConfig(k: 'current-peer-tab').obs;
visibleOrderedTabs = entries
.where((e) => !isTabHidden(e.name))
.map((e) => e.name)
.toList()
.obs;
try {
final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order');
if (conf.isNotEmpty) {
final json = jsonDecode(conf);
if (json is List) {
final List<String> list = json.map((e) => e.toString()).toList();
if (list.length == visibleOrderedTabs.length &&
visibleOrderedTabs.every((e) => list.contains(e))) {
visibleOrderedTabs.value = list;
}
}
}
} catch (e) {
debugPrintStack(label: '$e');
}
adjustTab();
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
@@ -105,10 +170,11 @@ class _PeerTabPageState extends State<PeerTabPage>
super.initState();
}
Future<void> handleTabSelection(String tabName) async {
currentTab.value = tabName;
await bind.setLocalFlutterConfig(k: 'current-peer-tab', v: tabName);
entries.firstWhereOrNull((e) => e.name == tabName)?.load();
Future<void> handleTabSelection(int tabIndex) async {
if (tabIndex < entries.length) {
statePeerTab.currentTab.value = tabIndex;
entries[tabIndex].load();
}
}
@override
@@ -149,65 +215,80 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createSwitchBar(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return Obx(() {
int indexCounter = -1;
return ReorderableListView(
buildDefaultDragHandles: false,
onReorder: (oldIndex, newIndex) {
var list = visibleOrderedTabs.toList();
if (oldIndex < newIndex) {
newIndex -= 1;
}
final String item = list.removeAt(oldIndex);
list.insert(newIndex, item);
bind.setLocalFlutterConfig(
k: 'peer-tab-order', v: jsonEncode(list));
visibleOrderedTabs.value = list;
},
var tabs = statePeerTab.currentTabs();
return ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
scrollController: ScrollController(),
children: visibleOrderedTabs.map((t) {
indexCounter++;
return ReorderableDragStartListener(
key: ValueKey(t),
index: indexCounter,
child: InkWell(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: currentTab.value == t
? Theme.of(context).backgroundColor
: null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
physics: NeverScrollableScrollPhysics(),
controller: ScrollController(),
children: tabs.map((t) {
return InkWell(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: statePeerTab.currentTab.value == t
? Theme.of(context).backgroundColor
: null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
),
child: Align(
alignment: Alignment.center,
child: Text(
translatedTabname(t),
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color: statePeerTab.currentTab.value == t
? textColor
: textColor
?..withOpacity(0.5)),
),
child: Align(
alignment: Alignment.center,
child: Text(
translate(t),
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color: currentTab.value == t ? textColor : textColor
?..withOpacity(0.5)),
),
)),
onTap: () async => await handleTabSelection(t),
),
)),
onTap: () async {
await handleTabSelection(t);
await bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: t.toString());
},
);
}).toList());
});
}
translatedTabname(int index) {
if (index < statePeerTab.tabNames.length) {
final name = statePeerTab.tabNames[index];
if (index == groupTabIndex) {
if (name == defaultGroupTabname) {
return translate(name);
} else {
return name;
}
} else {
return translate(name);
}
}
assert(false);
return index.toString();
}
Widget _createPeersView() {
final verticalMargin = isDesktop ? 12.0 : 6.0;
return Expanded(
child: Obx(() =>
entries.firstWhereOrNull((e) => e.name == currentTab.value)?.widget ??
visibleContextMenuListener(Center(
child: Text(translate('Right click to select tabs')),
))).marginSymmetric(vertical: verticalMargin),
);
child: Obx(() {
var tabs = statePeerTab.currentTabs();
if (tabs.isEmpty) {
return visibleContextMenuListener(Center(
child: Text(translate('Right click to select tabs')),
));
} else {
if (tabs.contains(statePeerTab.currentTab.value)) {
return entries[statePeerTab.currentTab.value].widget;
} else {
statePeerTab.currentTab.value = tabs[0];
return entries[statePeerTab.currentTab.value].widget;
}
}
}).marginSymmetric(vertical: verticalMargin));
}
Widget _createPeerViewTypeSwitch(BuildContext context) {
@@ -240,22 +321,10 @@ class _PeerTabPageState extends State<PeerTabPage>
);
}
bool isTabHidden(String name) {
int index = entries.indexWhere((e) => e.name == name);
if (index >= 0) {
return tabHiddenFlag & (1 << index) != 0;
}
assert(false);
return false;
}
adjustTab() {
if (visibleOrderedTabs.isNotEmpty) {
if (!visibleOrderedTabs.contains(currentTab.value)) {
handleTabSelection(visibleOrderedTabs[0]);
}
} else {
currentTab.value = '';
var tabs = statePeerTab.currentTabs();
if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) {
statePeerTab.currentTab.value = tabs[0];
}
}
@@ -278,47 +347,44 @@ class _PeerTabPageState extends State<PeerTabPage>
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
final List<MenuEntryBase> menu = entries.asMap().entries.map((e) {
int bitMask = 1 << e.key;
return MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: translate(e.value.name),
getter: () async {
return tabHiddenFlag.value & bitMask == 0;
},
setter: (show) async {
if (show) {
tabHiddenFlag.value &= ~bitMask;
} else {
tabHiddenFlag.value |= bitMask;
}
await bind.setLocalFlutterConfig(
k: 'hidden-peer-card', v: tabHiddenFlag.value.toRadixString(2));
visibleOrderedTabs.removeWhere((e) => isTabHidden(e));
visibleOrderedTabs.addAll(entries
.where((e) =>
!visibleOrderedTabs.contains(e.name) &&
!isTabHidden(e.name))
.map((e) => e.name)
.toList());
await bind.setLocalFlutterConfig(
k: 'peer-tab-order', v: jsonEncode(visibleOrderedTabs));
cancelFunc();
adjustTab();
});
}).toList();
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList(),
);
return Obx(() {
final List<MenuEntryBase> menu = List.empty(growable: true);
for (int i = 0; i < statePeerTab.tabNames.length; i++) {
if (i == groupTabIndex && statePeerTab.filterGroupCard()) {
continue;
}
int bitMask = 1 << i;
menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: translatedTabname(i),
getter: () async {
return statePeerTab.tabHiddenFlag & bitMask == 0;
},
setter: (show) async {
if (show) {
statePeerTab.tabHiddenFlag.value &= ~bitMask;
} else {
statePeerTab.tabHiddenFlag.value |= bitMask;
}
await bind.setLocalFlutterConfig(
k: 'hidden-peer-card',
v: statePeerTab.tabHiddenFlag.value.toRadixString(2));
cancelFunc();
adjustTab();
}));
}
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList());
});
}
}

View File

@@ -326,3 +326,21 @@ class AddressBookPeersView extends BasePeersView {
return true;
}
}
class MyGroupPeerView extends BasePeersView {
MyGroupPeerView(
{Key? key,
EdgeInsets? menuPadding,
ScrollController? scrollController,
required List<Peer> initPeers})
: super(
key: key,
name: 'my group peer',
loadEvent: 'load_my_group_peers',
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer,
menuPadding: menuPadding,
),
initPeers: initPeers,
);
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/state_model.dart';
import '../../models/input_model.dart';
@@ -9,11 +10,12 @@ class RawKeyFocusScope extends StatelessWidget {
final InputModel inputModel;
final Widget child;
RawKeyFocusScope(
{this.focusNode,
this.onFocusChange,
required this.inputModel,
required this.child});
RawKeyFocusScope({
this.focusNode,
this.onFocusChange,
required this.inputModel,
required this.child,
});
@override
Widget build(BuildContext context) {
@@ -24,7 +26,8 @@ class RawKeyFocusScope extends StatelessWidget {
canRequestFocus: true,
focusNode: focusNode,
onFocusChange: onFocusChange,
onKey: inputModel.handleRawKeyEvent,
onKey:
stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null,
child: child));
}
}
@@ -35,11 +38,15 @@ class RawPointerMouseRegion extends StatelessWidget {
final MouseCursor? cursor;
final PointerEnterEventListener? onEnter;
final PointerExitEventListener? onExit;
final PointerDownEventListener? onPointerDown;
final PointerUpEventListener? onPointerUp;
RawPointerMouseRegion(
{this.onEnter,
this.onExit,
this.cursor,
this.onPointerDown,
this.onPointerUp,
required this.inputModel,
required this.child});
@@ -47,8 +54,14 @@ class RawPointerMouseRegion extends StatelessWidget {
Widget build(BuildContext context) {
return Listener(
onPointerHover: inputModel.onPointHoverImage,
onPointerDown: inputModel.onPointDownImage,
onPointerUp: inputModel.onPointUpImage,
onPointerDown: (evt) {
onPointerDown?.call(evt);
inputModel.onPointDownImage(evt);
},
onPointerUp: (evt) {
onPointerUp?.call(evt);
inputModel.onPointUpImage(evt);
},
onPointerMove: inputModel.onPointMoveImage,
onPointerSignal: inputModel.onPointerSignalImage,
/*

View File

@@ -5,6 +5,11 @@ import 'package:flutter_hbb/common.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
const String kPeerPlatformAndroid = "Android";
/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page"
const String kAppTypeMain = "main";
const String kAppTypeDesktopRemote = "remote";
@@ -100,6 +105,8 @@ const kRemoteImageQualityLow = 'low';
/// [kRemoteImageQualityCustom] Custom image quality.
const kRemoteImageQualityCustom = 'custom';
const kIgnoreDpi = true;
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
/// see [LogicalKeyboardKey.keyLabel]
const Map<int, String> logicalKeyMap = <int, String>{

View File

@@ -6,7 +6,6 @@ import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:get/get.dart';
@@ -16,7 +15,6 @@ import 'package:window_manager/window_manager.dart';
import '../../common.dart';
import '../../common/formatter/id_formatter.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/peers_view.dart';
import '../../models/platform_model.dart';
import '../widgets/button.dart';
@@ -46,7 +44,7 @@ class _ConnectionPageState extends State<ConnectionPage>
var svcStatusCode = 0.obs;
var svcIsUsingPublicServer = true.obs;
bool isWindowMinisized = false;
bool isWindowMinimized = false;
@override
void initState() {
@@ -82,13 +80,13 @@ class _ConnectionPageState extends State<ConnectionPage>
void onWindowEvent(String eventName) {
super.onWindowEvent(eventName);
if (eventName == 'minimize') {
isWindowMinisized = true;
isWindowMinimized = true;
} else if (eventName == 'maximize' || eventName == 'restore') {
if (isWindowMinisized && Platform.isWindows) {
// windows can't update when minisized.
if (isWindowMinimized && Platform.isWindows) {
// windows can't update when minimized.
Get.forceAppUpdate();
}
isWindowMinisized = false;
isWindowMinimized = false;
}
}
@@ -172,6 +170,7 @@ class _ConnectionPageState extends State<ConnectionPage>
Expanded(
child: Obx(
() => TextField(
maxLength: 90,
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
@@ -179,12 +178,13 @@ class _ConnectionPageState extends State<ConnectionPage>
style: const TextStyle(
fontFamily: 'WorkSans',
fontSize: 22,
height: 1,
height: 1.25,
),
maxLines: 1,
cursorColor:
Theme.of(context).textTheme.titleLarge?.color,
decoration: InputDecoration(
counterText: '',
hintText: _idInputFocused.value
? null
: translate('Enter Remote ID'),

View File

@@ -42,6 +42,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
var svcStopped = false.obs;
var watchIsCanScreenRecording = false;
var watchIsProcessTrust = false;
var watchIsInputMonitoring = false;
Timer? _updateTimer;
@override
@@ -334,6 +335,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
bind.mainIsProcessTrusted(prompt: true);
watchIsProcessTrust = true;
}, help: 'Help', link: translate("doc_mac_permission"));
} else if (!bind.mainIsCanInputMonitoring(prompt: false)) {
return buildInstallCard("Permissions", "config_input", "Configure",
() async {
bind.mainIsCanInputMonitoring(prompt: true);
watchIsInputMonitoring = true;
}, help: 'Help', link: translate("doc_mac_permission"));
} else if (!svcStopped.value &&
bind.mainIsInstalled() &&
!bind.mainIsInstalledDaemon(prompt: false)) {
@@ -438,7 +445,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
@override
void initState() {
super.initState();
bind.mainStartGrabKeyboard();
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
await gFFI.serverModel.fetchID();
final url = await bind.mainGetSoftwareUpdateUrl();
@@ -468,6 +474,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
setState(() {});
}
}
if (watchIsInputMonitoring) {
if (bind.mainIsCanInputMonitoring(prompt: false)) {
watchIsInputMonitoring = false;
setState(() {});
}
}
});
Get.put<RxBool>(svcStopped, tag: 'stop-service');
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
@@ -501,9 +513,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventShow) {
rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
await rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
} else if (call.method == kWindowEventHide) {
rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
await rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
} else if (call.method == kWindowConnect) {
await connectMainDesktop(
call.arguments['id'],

View File

@@ -8,7 +8,6 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/widgets/login.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:get/get.dart';
@@ -18,6 +17,7 @@ import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
const double _kTabWidth = 235;
const double _kTabHeight = 42;
@@ -125,6 +125,7 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
scrollController: controller,
child: PageView(
controller: controller,
physics: NeverScrollableScrollPhysics(),
children: const [
_General(),
_Safety(),
@@ -273,6 +274,15 @@ class _GeneralState extends State<_General> {
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
'enable-confirm-closing-tabs'),
_OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'),
if (Platform.isLinux)
Tooltip(
message: translate('software_render_tip'),
child: _OptionCheckBox(
context,
"Always use software rendering",
'allow-always-software-render',
),
)
]);
}
@@ -932,6 +942,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
return false;
}
}
final old = await bind.mainGetOption(key: 'custom-rendezvous-server');
if (old.isNotEmpty && old != idServer) {
await gFFI.userModel.logOut();
}
// should set one by one
await bind.mainSetOption(
key: 'custom-rendezvous-server', value: idServer);
@@ -954,23 +968,17 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
import() {
Clipboard.getData(Clipboard.kTextPlain).then((value) {
TextEditingController mytext = TextEditingController();
String? aNullableString = '';
aNullableString = value?.text;
mytext.text = aNullableString.toString();
if (mytext.text.isNotEmpty) {
final text = value?.text;
if (text != null && text.isNotEmpty) {
try {
Map<String, dynamic> config = jsonDecode(mytext.text);
if (config.containsKey('IdServer')) {
String id = config['IdServer'] ?? '';
String relay = config['RelayServer'] ?? '';
String api = config['ApiServer'] ?? '';
String key = config['Key'] ?? '';
idController.text = id;
relayController.text = relay;
apiController.text = api;
keyController.text = key;
Future<bool> success = set(id, relay, api, key);
final sc = ServerConfig.decode(text);
if (sc.idServer.isNotEmpty) {
idController.text = sc.idServer;
relayController.text = sc.relayServer;
apiController.text = sc.apiServer;
keyController.text = sc.key;
Future<bool> success =
set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
success.then((value) {
if (value) {
showToast(
@@ -992,12 +1000,15 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
}
export() {
Map<String, String> config = {};
config['IdServer'] = idController.text.trim();
config['RelayServer'] = relayController.text.trim();
config['ApiServer'] = apiController.text.trim();
config['Key'] = keyController.text.trim();
Clipboard.setData(ClipboardData(text: jsonEncode(config)));
final text = ServerConfig(
idServer: idController.text,
relayServer: relayController.text,
apiServer: apiController.text,
key: keyController.text)
.encode();
debugPrint("ServerConfig export: $text");
Clipboard.setData(ClipboardData(text: text));
showToast(translate('Export server configuration successfully'));
}
@@ -1059,21 +1070,13 @@ class _AccountState extends State<_Account> {
}
Widget accountAction() {
return _futureBuilder(future: () async {
return await gFFI.userModel.getUserName();
}(), hasData: (_) {
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog().then((success) {
if (success) {
gFFI.abModel.pullAb();
}
})
: gFFI.userModel.logOut()
}));
});
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog()
: gFFI.userModel.logOut()
}));
}
}
@@ -1103,29 +1106,31 @@ class _AboutState extends State<_About> {
child: SingleChildScrollView(
controller: scrollController,
physics: NeverScrollableScrollPhysics(),
child: _Card(title: 'About RustDesk', children: [
child: _Card(title: '${translate('About')} RustDesk', children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
),
Text('Version: $version').marginSymmetric(vertical: 4.0),
Text('Build Date: $buildDate').marginSymmetric(vertical: 4.0),
Text('${translate('Version')}: $version')
.marginSymmetric(vertical: 4.0),
Text('${translate('Build Date')}: $buildDate')
.marginSymmetric(vertical: 4.0),
InkWell(
onTap: () {
launchUrlString('https://rustdesk.com/privacy');
},
child: const Text(
'Privacy Statement',
child: Text(
translate('Privacy Statement'),
style: linkStyle,
).marginSymmetric(vertical: 4.0)),
InkWell(
onTap: () {
launchUrlString('https://rustdesk.com');
},
child: const Text(
'Website',
child: Text(
translate('Website'),
style: linkStyle,
).marginSymmetric(vertical: 4.0)),
Container(
@@ -1142,8 +1147,8 @@ class _AboutState extends State<_About> {
'Copyright © 2022 Purslane Ltd.\n$license',
style: const TextStyle(color: Colors.white),
),
const Text(
'Made with heart in this chaotic world!',
Text(
translate('Slogan_tip'),
style: TextStyle(
fontWeight: FontWeight.w800,
color: Colors.white),
@@ -1227,7 +1232,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
ref.value = option;
if (reverse) option = !option;
String value = bool2option(key, option);
bind.mainSetOption(key: key, value: value);
await bind.mainSetOption(key: key, value: value);
update?.call();
}
}
@@ -1431,7 +1436,7 @@ Widget _lock(
_LabeledTextField(
BuildContext context,
String lable,
String label,
TextEditingController controller,
String errorText,
bool enabled,
@@ -1442,7 +1447,7 @@ _LabeledTextField(
Expanded(
flex: 4,
child: Text(
'${translate(lable)}:',
'${translate(label)}:',
textAlign: TextAlign.right,
style: TextStyle(color: _disabledTextColor(context, enabled)),
),
@@ -1455,6 +1460,8 @@ _LabeledTextField(
enabled: enabled,
obscureText: secure,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 15),
errorText: errorText.isNotEmpty ? errorText : null),
style: TextStyle(
color: _disabledTextColor(context, enabled),

View File

@@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:get/get.dart';
@@ -32,6 +33,18 @@ enum LocationStatus {
fileSearchBar
}
/// The status of currently focused scope of the mouse
enum MouseFocusScope {
/// Mouse is in local field.
local,
/// Mouse is in remote field.
remote,
/// Mouse is not in local field, remote neither.
none
}
class FileManagerPage extends StatefulWidget {
const FileManagerPage({Key? key, required this.id}) : super(key: key);
final String id;
@@ -55,6 +68,11 @@ class _FileManagerPageState extends State<FileManagerPage>
final _searchTextRemote = "".obs;
final _breadCrumbScrollerLocal = ScrollController();
final _breadCrumbScrollerRemote = ScrollController();
final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
final _keyboardNodeLocal = FocusNode(debugLabel: "keyboardNodeLocal");
final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote");
final _listSearchBufferLocal = TimeoutStringBuffer();
final _listSearchBufferRemote = TimeoutStringBuffer();
/// [_lastClickTime], [_lastClickEntry] help to handle double click
int _lastClickTime =
@@ -197,6 +215,7 @@ class _FileManagerPageState extends State<FileManagerPage>
}
Widget body({bool isLocal = false}) {
final scrollController = ScrollController();
return Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
margin: const EdgeInsets.all(16.0),
@@ -217,8 +236,8 @@ class _FileManagerPageState extends State<FileManagerPage>
children: [
Expanded(
child: SingleChildScrollView(
controller: ScrollController(),
child: _buildDataTable(context, isLocal),
controller: scrollController,
child: _buildDataTable(context, isLocal, scrollController),
),
)
],
@@ -228,7 +247,9 @@ class _FileManagerPageState extends State<FileManagerPage>
);
}
Widget _buildDataTable(BuildContext context, bool isLocal) {
Widget _buildDataTable(
BuildContext context, bool isLocal, ScrollController scrollController) {
const rowHeight = 25.0;
final fd = model.getCurrentDir(isLocal);
final entries = fd.entries;
final sortIndex = (SortBy style) {
@@ -246,130 +267,219 @@ class _FileManagerPageState extends State<FileManagerPage>
final sortAscending =
isLocal ? model.localSortAscending : model.remoteSortAscending;
return ObxValue<RxString>(
(searchText) {
final filteredEntries = searchText.isNotEmpty
? entries.where((element) {
return element.name.contains(searchText.value);
}).toList(growable: false)
: entries;
return DataTable(
key: ValueKey(isLocal ? 0 : 1),
showCheckboxColumn: false,
dataRowHeight: 25,
headingRowHeight: 30,
horizontalMargin: 8,
columnSpacing: 8,
showBottomBorder: true,
sortColumnIndex: sortIndex,
sortAscending: sortAscending,
columns: [
DataColumn(
label: Text(
translate("Name"),
).marginSymmetric(horizontal: 4),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.name,
isLocal: isLocal, ascending: ascending);
}),
DataColumn(
label: Text(
translate("Modified"),
),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.modified,
isLocal: isLocal, ascending: ascending);
}),
DataColumn(
label: Text(translate("Size")),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.size,
isLocal: isLocal, ascending: ascending);
}),
],
rows: filteredEntries.map((entry) {
final sizeStr =
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
final lastModifiedStr = entry.isDrive
? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
return DataRow(
key: ValueKey(entry.name),
onSelectChanged: (s) {
_onSelectedChanged(getSelectedItems(isLocal), filteredEntries,
entry, isLocal);
},
selected: getSelectedItems(isLocal).contains(entry),
cells: [
DataCell(
Container(
width: 200,
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: entry.name,
child: Row(children: [
entry.isDrive
? Image(
image: iconHardDrive,
fit: BoxFit.scaleDown,
return MouseRegion(
onEnter: (evt) {
_mouseFocusScope.value =
isLocal ? MouseFocusScope.local : MouseFocusScope.remote;
if (isLocal) {
_keyboardNodeLocal.requestFocus();
} else {
_keyboardNodeRemote.requestFocus();
}
},
onExit: (evt) {
_mouseFocusScope.value = MouseFocusScope.none;
},
child: ListSearchActionListener(
node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote,
buffer: isLocal ? _listSearchBufferLocal : _listSearchBufferRemote,
onNext: (buffer) {
debugPrint("searching next for $buffer");
assert(buffer.length == 1);
final selectedEntries = getSelectedItems(isLocal);
assert(selectedEntries.length <= 1);
var skipCount = 0;
if (selectedEntries.items.isNotEmpty) {
final index = entries.indexOf(selectedEntries.items.first);
if (index < 0) {
return;
}
skipCount = index + 1;
}
var searchResult = entries
.skip(skipCount)
.where((element) => element.name.startsWith(buffer));
if (searchResult.isEmpty) {
// cannot find next, lets restart search from head
searchResult =
entries.where((element) => element.name.startsWith(buffer));
}
if (searchResult.isEmpty) {
setState(() {
getSelectedItems(isLocal).clear();
});
return;
}
_jumpToEntry(
isLocal, searchResult.first, scrollController, rowHeight, buffer);
},
onSearch: (buffer) {
debugPrint("searching for $buffer");
final selectedEntries = getSelectedItems(isLocal);
final searchResult =
entries.where((element) => element.name.startsWith(buffer));
selectedEntries.clear();
if (searchResult.isEmpty) {
setState(() {
getSelectedItems(isLocal).clear();
});
return;
}
_jumpToEntry(
isLocal, searchResult.first, scrollController, rowHeight, buffer);
},
child: ObxValue<RxString>(
(searchText) {
final filteredEntries = searchText.isNotEmpty
? entries.where((element) {
return element.name.contains(searchText.value);
}).toList(growable: false)
: entries;
return DataTable(
key: ValueKey(isLocal ? 0 : 1),
showCheckboxColumn: false,
dataRowHeight: rowHeight,
headingRowHeight: 30,
horizontalMargin: 8,
columnSpacing: 8,
showBottomBorder: true,
sortColumnIndex: sortIndex,
sortAscending: sortAscending,
columns: [
DataColumn(
label: Text(
translate("Name"),
).marginSymmetric(horizontal: 4),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.name,
isLocal: isLocal, ascending: ascending);
}),
DataColumn(
label: Text(
translate("Modified"),
),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.modified,
isLocal: isLocal, ascending: ascending);
}),
DataColumn(
label: Text(translate("Size")),
onSort: (columnIndex, ascending) {
model.changeSortStyle(SortBy.size,
isLocal: isLocal, ascending: ascending);
}),
],
rows: filteredEntries.map((entry) {
final sizeStr =
entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
final lastModifiedStr = entry.isDrive
? " "
: "${entry.lastModified().toString().replaceAll(".000", "")} ";
return DataRow(
key: ValueKey(entry.name),
onSelectChanged: (s) {
_onSelectedChanged(getSelectedItems(isLocal),
filteredEntries, entry, isLocal);
},
selected: getSelectedItems(isLocal).contains(entry),
cells: [
DataCell(
Container(
width: 200,
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: entry.name,
child: Row(children: [
entry.isDrive
? Image(
image: iconHardDrive,
fit: BoxFit.scaleDown,
color: Theme.of(context)
.iconTheme
.color
?.withOpacity(0.7))
.paddingAll(4)
: Icon(
entry.isFile
? Icons.feed_outlined
: Icons.folder,
size: 20,
color: Theme.of(context)
.iconTheme
.color
?.withOpacity(0.7))
.paddingAll(4)
: Icon(
entry.isFile
? Icons.feed_outlined
: Icons.folder,
size: 20,
color: Theme.of(context)
.iconTheme
.color
?.withOpacity(0.7),
).marginSymmetric(horizontal: 2),
Expanded(
child: Text(entry.name,
overflow: TextOverflow.ellipsis))
]),
)),
onTap: () {
final items = getSelectedItems(isLocal);
?.withOpacity(0.7),
).marginSymmetric(horizontal: 2),
Expanded(
child: Text(entry.name,
overflow: TextOverflow.ellipsis))
]),
)),
onTap: () {
final items = getSelectedItems(isLocal);
// handle double click
if (_checkDoubleClick(entry)) {
openDirectory(entry.path, isLocal: isLocal);
items.clear();
return;
}
_onSelectedChanged(
items, filteredEntries, entry, isLocal);
},
),
DataCell(FittedBox(
child: Tooltip(
// handle double click
if (_checkDoubleClick(entry)) {
openDirectory(entry.path, isLocal: isLocal);
items.clear();
return;
}
_onSelectedChanged(
items, filteredEntries, entry, isLocal);
},
),
DataCell(FittedBox(
child: Tooltip(
waitDuration: Duration(milliseconds: 500),
message: lastModifiedStr,
child: Text(
lastModifiedStr,
style: TextStyle(
fontSize: 12, color: MyTheme.darkGray),
)))),
DataCell(Tooltip(
waitDuration: Duration(milliseconds: 500),
message: lastModifiedStr,
message: sizeStr,
child: Text(
lastModifiedStr,
sizeStr,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12, color: MyTheme.darkGray),
)))),
DataCell(Tooltip(
waitDuration: Duration(milliseconds: 500),
message: sizeStr,
child: Text(
sizeStr,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 10, color: MyTheme.darkGray),
))),
]);
}).toList(growable: false),
);
},
isLocal ? _searchTextLocal : _searchTextRemote,
fontSize: 10, color: MyTheme.darkGray),
))),
]);
}).toList(growable: false),
);
},
isLocal ? _searchTextLocal : _searchTextRemote,
),
),
);
}
void _jumpToEntry(bool isLocal, Entry entry,
ScrollController scrollController, double rowHeight, String buffer) {
final entries = model.getCurrentDir(isLocal).entries;
final index = entries.indexOf(entry);
if (index == -1) {
debugPrint("entry is not valid: ${entry.path}");
}
final selectedEntries = getSelectedItems(isLocal);
final searchResult =
entries.where((element) => element.name.startsWith(buffer));
selectedEntries.clear();
if (searchResult.isEmpty) {
return;
}
final offset = min(
max(scrollController.position.minScrollExtent,
entries.indexOf(searchResult.first) * rowHeight),
scrollController.position.maxScrollExtent);
scrollController.jumpTo(offset);
setState(() {
selectedEntries.add(isLocal, searchResult.first);
debugPrint("focused on ${searchResult.first.name}");
});
}
void _onSelectedChanged(SelectedItems selectedItems, List<Entry> entries,
Entry entry, bool isLocal) {
final isCtrlDown = RawKeyboard.instance.keysPressed
@@ -872,6 +982,8 @@ class _FileManagerPageState extends State<FileManagerPage>
},
dismissOnClicked: true));
}
} catch (e) {
debugPrint("buildBread fetchDirectory err=$e");
} finally {
if (!isLocal) {
_ffi.dialogManager.dismissByTag(loadingTag);
@@ -1015,4 +1127,14 @@ class _FileManagerPageState extends State<FileManagerPage>
}
model.sendFiles(items, isRemote: false);
}
void refocusKeyboardListener(bool isLocal) {
Future.delayed(Duration.zero, () {
if (isLocal) {
_keyboardNodeLocal.requestFocus();
} else {
_keyboardNodeRemote.requestFocus();
}
});
}
}

View File

@@ -127,8 +127,8 @@ class _PortForwardPageState extends State<PortForwardPage>
}
buildTunnel(BuildContext context) {
text(String lable) => Expanded(
child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
text(String label) => Expanded(
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
return Theme(
data: Theme.of(context)
@@ -241,8 +241,8 @@ class _PortForwardPageState extends State<PortForwardPage>
}
Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) {
text(String lable) => Expanded(
child: Text(lable, style: const TextStyle(fontSize: 20))
text(String label) => Expanded(
child: Text(label, style: const TextStyle(fontSize: 20))
.marginOnly(left: _kTextLeftMargin));
return Container(
@@ -285,11 +285,11 @@ class _PortForwardPageState extends State<PortForwardPage>
}
buildRdp(BuildContext context) {
text1(String lable) => Expanded(
child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
text2(String lable) => Expanded(
text1(String label) => Expanded(
child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
text2(String label) => Expanded(
child: Text(
lable,
label,
style: const TextStyle(fontSize: 20),
).marginOnly(left: _kTextLeftMargin));
return Theme(

View File

@@ -2,9 +2,12 @@ import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_custom_cursor/cursor_manager.dart'
as custom_cursor_manager;
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart';
@@ -20,6 +23,7 @@ import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import '../widgets/remote_menubar.dart';
import '../widgets/kb_layout_type_chooser.dart';
bool _isCustomCursorInited = false;
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
@@ -46,17 +50,17 @@ class RemotePage extends StatefulWidget {
}
class _RemotePageState extends State<RemotePage>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, MultiWindowListener {
Timer? _timer;
String keyboardMode = "legacy";
bool _isWindowBlur = false;
final _cursorOverImage = false.obs;
late RxBool _showRemoteCursor;
late RxBool _zoomCursor;
late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled;
final FocusNode _rawKeyFocusNode = FocusNode();
var _imageFocused = false;
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
Function(bool)? _onEnterOrLeaveImage4Menubar;
@@ -92,6 +96,10 @@ class _RemotePageState extends State<RemotePage>
_initStates(widget.id);
_ffi = FFI();
Get.put(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
});
_ffi.start(widget.id);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@@ -101,7 +109,6 @@ class _RemotePageState extends State<RemotePage>
if (!Platform.isLinux) {
Wakelock.enable();
}
_rawKeyFocusNode.requestFocus();
_ffi.ffiModel.updateEventListener(widget.id);
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
// Session option should be set after models.dart/FFI.start
@@ -109,22 +116,59 @@ class _RemotePageState extends State<RemotePage>
id: widget.id, arg: 'show-remote-cursor');
_zoomCursor.value =
bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor');
if (!_isCustomCursorInited) {
customCursorController.registerNeedUpdateCursorCallback(
(String? lastKey, String? currentKey) async {
if (_firstEnterImage.value) {
_firstEnterImage.value = false;
return true;
}
return lastKey == null || lastKey != currentKey;
});
_isCustomCursorInited = true;
DesktopMultiWindow.addListener(this);
// if (!_isCustomCursorInited) {
// customCursorController.registerNeedUpdateCursorCallback(
// (String? lastKey, String? currentKey) async {
// if (_firstEnterImage.value) {
// _firstEnterImage.value = false;
// return true;
// }
// return lastKey == null || lastKey != currentKey;
// });
// _isCustomCursorInited = true;
// }
}
@override
void onWindowBlur() {
super.onWindowBlur();
// On windows, we use `focus` way to handle keyboard better.
// Now on Linux, there's some rdev issues which will break the input.
// We disable the `focus` way for non-Windows temporarily.
if (Platform.isWindows) {
_isWindowBlur = true;
// unfocus the primary-focus when the whole window is lost focus,
// and let OS to handle events instead.
_rawKeyFocusNode.unfocus();
}
}
@override
void onWindowFocus() {
super.onWindowFocus();
// See [onWindowBlur].
if (Platform.isWindows) {
_isWindowBlur = false;
}
}
@override
void onWindowRestore() {
super.onWindowRestore();
// On windows, we use `onWindowRestore` way to handle window restore from
// a minimized state.
if (Platform.isWindows) {
_isWindowBlur = false;
}
}
@override
void dispose() {
debugPrint("REMOTE PAGE dispose ${widget.id}");
// ensure we leave this session, this is a double check
bind.sessionEnterOrLeave(id: widget.id, enter: false);
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.recordingModel.onClose();
_rawKeyFocusNode.dispose();
@@ -153,8 +197,23 @@ class _RemotePageState extends State<RemotePage>
color: Colors.black,
child: RawKeyFocusScope(
focusNode: _rawKeyFocusNode,
onFocusChange: (bool v) {
_imageFocused = v;
onFocusChange: (bool imageFocused) {
debugPrint(
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
// See [onWindowBlur].
if (Platform.isWindows) {
if (_isWindowBlur) {
imageFocused = false;
Future.delayed(Duration.zero, () {
_rawKeyFocusNode.unfocus();
});
}
if (imageFocused) {
_ffi.inputModel.enterOrLeave(true);
} else {
_ffi.inputModel.enterOrLeave(false);
}
}
},
inputModel: _ffi.inputModel,
child: getBodyForDesktop(context)));
@@ -181,9 +240,6 @@ class _RemotePageState extends State<RemotePage>
}
void enterView(PointerEnterEvent evt) {
if (!_imageFocused) {
_rawKeyFocusNode.requestFocus();
}
_cursorOverImage.value = true;
_firstEnterImage.value = true;
if (_onEnterOrLeaveImage4Menubar != null) {
@@ -193,7 +249,13 @@ class _RemotePageState extends State<RemotePage>
//
}
}
_ffi.inputModel.enterOrLeave(true);
// See [onWindowBlur].
if (!Platform.isWindows) {
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
bind.sessionEnterOrLeave(id: widget.id, enter: true);
}
}
void leaveView(PointerExitEvent evt) {
@@ -206,7 +268,10 @@ class _RemotePageState extends State<RemotePage>
//
}
}
_ffi.inputModel.enterOrLeave(false);
// See [onWindowBlur].
if (!Platform.isWindows) {
bind.sessionEnterOrLeave(id: widget.id, enter: false);
}
}
Widget getBodyForDesktop(BuildContext context) {
@@ -228,6 +293,21 @@ class _RemotePageState extends State<RemotePage>
listenerBuilder: (child) => RawPointerMouseRegion(
onEnter: enterView,
onExit: leaveView,
onPointerDown: (event) {
// A double check for blur status.
// Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
// Sometimes the system does not send the necessary focus event to flutter. We should manually
// handle this inconsistent status by setting `_isWindowBlur` to false. So we can
// ensure the grab-key thread is running when our users are clicking the remote canvas.
if (_isWindowBlur) {
debugPrint(
"Unexpected status: onPointerDown is triggered while the remote window is in blur status");
_isWindowBlur = false;
}
if (!_rawKeyFocusNode.hasFocus) {
_rawKeyFocusNode.requestFocus();
}
},
inputModel: _ffi.inputModel,
child: child,
),
@@ -235,9 +315,9 @@ class _RemotePageState extends State<RemotePage>
}))
];
if (!_ffi.canvasModel.cursorEmbeded) {
paints.add(Obx(() => Visibility(
visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue,
if (!_ffi.canvasModel.cursorEmbedded) {
paints.add(Obx(() => Offstage(
offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse,
child: CursorPaint(
id: widget.id,
zoomCursor: _zoomCursor,
@@ -302,7 +382,7 @@ class _ImagePaintState extends State<ImagePaint> {
mouseRegion({child}) => Obx(() => MouseRegion(
cursor: cursorOverImage.isTrue
? c.cursorEmbeded
? c.cursorEmbedded
? SystemMouseCursors.none
: keyboardEnabled.isTrue
? (() {
@@ -322,35 +402,36 @@ class _ImagePaintState extends State<ImagePaint> {
onHover: (evt) {},
child: child));
if (c.scrollStyle == ScrollStyle.scrollbar) {
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final imageWidth = c.getDisplayWidth() * s;
final imageHeight = c.getDisplayHeight() * s;
final imageSize = Size(imageWidth, imageHeight);
final imageWidget = CustomPaint(
size: Size(imageWidth, imageHeight),
size: imageSize,
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
);
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
final percentX = _horizontal.hasClients
? _horizontal.position.extentBefore /
(_horizontal.position.extentBefore +
_horizontal.position.extentInside +
_horizontal.position.extentAfter)
: 0.0;
final percentY = _vertical.hasClients
? _vertical.position.extentBefore /
(_vertical.position.extentBefore +
_vertical.position.extentInside +
_vertical.position.extentAfter)
: 0.0;
c.setScrollPercent(percentX, percentY);
return false;
},
child: mouseRegion(
child: _buildCrossScrollbar(context, _buildListener(imageWidget),
Size(imageWidth, imageHeight))),
);
onNotification: (notification) {
final percentX = _horizontal.hasClients
? _horizontal.position.extentBefore /
(_horizontal.position.extentBefore +
_horizontal.position.extentInside +
_horizontal.position.extentAfter)
: 0.0;
final percentY = _vertical.hasClients
? _vertical.position.extentBefore /
(_vertical.position.extentBefore +
_vertical.position.extentInside +
_vertical.position.extentAfter)
: 0.0;
c.setScrollPercent(percentX, percentY);
return false;
},
child: mouseRegion(
child: Obx(() => _buildCrossScrollbarFromLayout(
context, _buildListener(imageWidget), c.size, imageSize)),
));
} else {
final imageWidget = CustomPaint(
size: Size(c.size.width, c.size.height),
@@ -366,15 +447,23 @@ class _ImagePaintState extends State<ImagePaint> {
return MouseCursor.defer;
} else {
final key = cache.updateGetKey(scale, zoomCursor.value);
cursor.addKey(key);
return FlutterCustomMemoryImageCursor(
pixbuf: cache.data,
key: key,
hotx: cache.hotx,
hoty: cache.hoty,
imageWidth: (cache.width * cache.scale).toInt(),
imageHeight: (cache.height * cache.scale).toInt(),
);
if (!cursor.cachedKeys.contains(key)) {
debugPrint("Register custom cursor with key $key");
// [Safety]
// It's ok to call async registerCursor in current synchronous context,
// because activating the cursor is also an async call and will always
// be executed after this.
custom_cursor_manager.CursorManager.instance
.registerCursor(custom_cursor_manager.CursorData()
..buffer = cache.data!
..height = (cache.height * cache.scale).toInt()
..width = (cache.width * cache.scale).toInt()
..hotX = cache.hotx
..hotY = cache.hoty
..name = key);
cursor.addKey(key);
}
return FlutterCustomMemoryImageCursor(key: key);
}
}
@@ -477,24 +566,6 @@ class _ImagePaintState extends State<ImagePaint> {
return widget;
}
Widget _buildCrossScrollbar(BuildContext context, Widget child, Size size) {
var layoutSize = MediaQuery.of(context).size;
// If minimized, w or h may be negative here.
final w = layoutSize.width - kWindowBorderWidth * 2;
final h =
layoutSize.height - kWindowBorderWidth * 2 - kDesktopRemoteTabBarHeight;
layoutSize = Size(
w < 0 ? 0 : w,
h < 0 ? 0 : h,
);
bool overflow =
layoutSize.width < size.width || layoutSize.height < size.height;
return overflow
? Obx(() =>
_buildCrossScrollbarFromLayout(context, child, layoutSize, size))
: _buildCrossScrollbarFromLayout(context, child, layoutSize, size);
}
Widget _buildListener(Widget child) {
if (listenerBuilder != null) {
return listenerBuilder!(child);
@@ -529,7 +600,8 @@ class CursorPaint extends StatelessWidget {
double cx = c.x;
double cy = c.y;
if (c.scrollStyle == ScrollStyle.scrollbar) {
if (c.viewStyle.style == kRemoteViewStyleOriginal &&
c.scrollStyle == ScrollStyle.scrollbar) {
final d = c.parent.target!.ffiModel.display;
final imageWidth = d.width * c.scale;
final imageHeight = d.height * c.scale;
@@ -538,7 +610,7 @@ class CursorPaint extends StatelessWidget {
}
double x = (m.x - hotx) * c.scale + cx;
double y = (m.y - hoty) * c.scale + cx;
double y = (m.y - hoty) * c.scale + cy;
double scale = 1.0;
if (zoomCursor.isTrue) {
x = m.x - hotx + cx / c.scale;

View File

@@ -257,7 +257,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
),
]);
if (!ffi.canvasModel.cursorEmbeded) {
if (!ffi.canvasModel.cursorEmbedded) {
menu.add(MenuEntryDivider<String>());
menu.add(() {
final state = ShowRemoteCursorState.find(key);
@@ -308,7 +308,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
dismissOnClicked: true,
));
if (pi.platform == 'Linux' || pi.sasEnabled) {
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
menu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',

View File

@@ -1,14 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:provider/provider.dart';
/// multi-tab desktop remote screen
class DesktopRemoteScreen extends StatelessWidget {
final Map<String, dynamic> params;
const DesktopRemoteScreen({Key? key, required this.params}) : super(key: key);
DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) {
if (!bind.mainStartGrabKeyboard()) {
stateGlobal.grabKeyboard = true;
}
}
@override
Widget build(BuildContext context) {

View File

@@ -0,0 +1,225 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import '../../common.dart';
typedef KBChosenCallback = Future<bool> Function(String);
const double _kImageMarginVertical = 6.0;
const double _kImageMarginHorizontal = 10.0;
const double _kImageBoarderWidth = 4.0;
const double _kImagePaddingWidth = 4.0;
const Color _kImageBorderColor = Color.fromARGB(125, 202, 247, 2);
const double _kBorderRadius = 6.0;
const String _kKBLayoutTypeISO = 'ISO';
const String _kKBLayoutTypeNotISO = 'Not ISO';
const _kKBLayoutImageMap = {
_kKBLayoutTypeISO: 'kb_layout_iso',
_kKBLayoutTypeNotISO: 'kb_layout_not_iso',
};
class _KBImage extends StatelessWidget {
final String kbLayoutType;
final double imageWidth;
final RxString chosenType;
const _KBImage({
Key? key,
required this.kbLayoutType,
required this.imageWidth,
required this.chosenType,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_kBorderRadius),
border: Border.all(
color: chosenType.value == kbLayoutType
? _kImageBorderColor
: Colors.transparent,
width: _kImageBoarderWidth,
),
),
margin: EdgeInsets.symmetric(
horizontal: _kImageMarginHorizontal,
vertical: _kImageMarginVertical,
),
padding: EdgeInsets.all(_kImagePaddingWidth),
child: SvgPicture.asset(
'assets/${_kKBLayoutImageMap[kbLayoutType] ?? ""}.svg',
width: imageWidth -
_kImageMarginHorizontal * 2 -
_kImagePaddingWidth * 2 -
_kImageBoarderWidth * 2,
),
);
});
}
}
class _KBChooser extends StatelessWidget {
final String kbLayoutType;
final double imageWidth;
final RxString chosenType;
final KBChosenCallback cb;
const _KBChooser({
Key? key,
required this.kbLayoutType,
required this.imageWidth,
required this.chosenType,
required this.cb,
}) : super(key: key);
@override
Widget build(BuildContext context) {
onChanged(String? v) async {
if (v != null) {
if (await cb(v)) {
chosenType.value = v;
}
}
}
return Column(
children: [
TextButton(
onPressed: () {
onChanged(kbLayoutType);
},
child: _KBImage(
kbLayoutType: kbLayoutType,
imageWidth: imageWidth,
chosenType: chosenType,
),
style: TextButton.styleFrom(padding: EdgeInsets.zero),
),
TextButton(
child: Row(
children: [
Obx(() => Radio(
splashRadius: 0,
value: kbLayoutType,
groupValue: chosenType.value,
onChanged: onChanged,
)),
Text(kbLayoutType),
],
),
onPressed: () {
onChanged(kbLayoutType);
},
),
],
);
}
}
class KBLayoutTypeChooser extends StatelessWidget {
final RxString chosenType;
final double width;
final double height;
final double dividerWidth;
final KBChosenCallback cb;
KBLayoutTypeChooser({
Key? key,
required this.chosenType,
required this.width,
required this.height,
required this.dividerWidth,
required this.cb,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final imageWidth = width / 2 - dividerWidth;
return SizedBox(
width: width,
height: height,
child: Center(
child: Row(
children: [
_KBChooser(
kbLayoutType: _kKBLayoutTypeISO,
imageWidth: imageWidth,
chosenType: chosenType,
cb: cb,
),
VerticalDivider(
width: dividerWidth * 2,
),
_KBChooser(
kbLayoutType: _kKBLayoutTypeNotISO,
imageWidth: imageWidth,
chosenType: chosenType,
cb: cb,
),
],
),
),
);
}
}
RxString KBLayoutType = ''.obs;
String getLocalPlatformForKBLayoutType(String peerPlatform) {
String localPlatform = '';
if (peerPlatform != kPeerPlatformMacOS) {
return localPlatform;
}
if (Platform.isWindows) {
localPlatform = kPeerPlatformWindows;
} else if (Platform.isLinux) {
localPlatform = kPeerPlatformLinux;
}
// to-do: web desktop support ?
return localPlatform;
}
showKBLayoutTypeChooserIfNeeded(
String peerPlatform,
OverlayDialogManager dialogManager,
) async {
final localPlatform = getLocalPlatformForKBLayoutType(peerPlatform);
if (localPlatform == '') {
return;
}
KBLayoutType.value = bind.getLocalKbLayoutType();
if (KBLayoutType.value == _kKBLayoutTypeISO ||
KBLayoutType.value == _kKBLayoutTypeNotISO) {
return;
}
showKBLayoutTypeChooser(localPlatform, dialogManager);
}
showKBLayoutTypeChooser(
String localPlatform,
OverlayDialogManager dialogManager,
) {
dialogManager.show((setState, close) {
return CustomAlertDialog(
title:
Text('${translate('Select local keyboard type')} ($localPlatform)'),
content: KBLayoutTypeChooser(
chosenType: KBLayoutType,
width: 360,
height: 200,
dividerWidth: 4.0,
cb: (String v) async {
await bind.setLocalKbLayoutType(kbLayoutType: v);
KBLayoutType.value = bind.getLocalKbLayoutType();
return v == KBLayoutType.value;
}),
actions: [msgBoxButton(translate('Close'), close)],
onCancel: close,
);
});
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
class ListSearchActionListener extends StatelessWidget {
final FocusNode node;
final TimeoutStringBuffer buffer;
final Widget child;
final Function(String) onNext;
final Function(String) onSearch;
const ListSearchActionListener(
{super.key,
required this.node,
required this.buffer,
required this.child,
required this.onNext,
required this.onSearch});
@mustCallSuper
@override
Widget build(BuildContext context) {
return KeyboardListener(
autofocus: true,
onKeyEvent: (kv) {
final ch = kv.character;
if (ch == null) {
return;
}
final action = buffer.input(ch);
switch (action) {
case ListSearchAction.search:
onSearch(buffer.buffer);
break;
case ListSearchAction.next:
onNext(buffer.buffer);
break;
}
},
focusNode: node,
child: child);
}
}
enum ListSearchAction { search, next }
class TimeoutStringBuffer {
var _buffer = "";
late DateTime _duration;
static int timeoutMilliSec = 1500;
String get buffer => _buffer;
TimeoutStringBuffer() {
_duration = DateTime.now();
}
ListSearchAction input(String ch) {
final curr = DateTime.now();
try {
if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) {
_buffer = ch;
return ListSearchAction.search;
} else {
if (ch == _buffer) {
return ListSearchAction.next;
} else {
_buffer += ch;
return ListSearchAction.search;
}
}
} finally {
_duration = curr;
}
}
}

View File

@@ -1,521 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
final kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0);
class _IconOP extends StatelessWidget {
final String icon;
final double iconWidth;
const _IconOP({Key? key, required this.icon, required this.iconWidth})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0),
child: SvgPicture.asset(
'assets/$icon.svg',
width: iconWidth,
),
);
}
}
class ButtonOP extends StatelessWidget {
final String op;
final RxString curOP;
final double iconWidth;
final Color primaryColor;
final double height;
final Function() onTap;
const ButtonOP({
Key? key,
required this.op,
required this.curOP,
required this.iconWidth,
required this.primaryColor,
required this.height,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(children: [
Expanded(
child: Container(
height: height,
padding: kMidButtonPadding,
child: Obx(() => ElevatedButton(
style: ElevatedButton.styleFrom(
primary: curOP.value.isEmpty || curOP.value == op
? primaryColor
: Colors.grey,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed:
curOP.value.isEmpty || curOP.value == op ? onTap : null,
child: Stack(children: [
Center(child: Text('${translate("Continue with")} $op')),
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: 120,
child: _IconOP(
icon: op,
iconWidth: iconWidth,
)),
),
]),
)),
),
)
]);
}
}
class ConfigOP {
final String op;
final double iconWidth;
ConfigOP({required this.op, required this.iconWidth});
}
class WidgetOP extends StatefulWidget {
final ConfigOP config;
final RxString curOP;
final Function(String) cbLogin;
const WidgetOP({
Key? key,
required this.config,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _WidgetOPState();
}
}
class _WidgetOPState extends State<WidgetOP> {
Timer? _updateTimer;
String _stateMsg = '';
String _FailedMsg = '';
String _url = '';
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
_updateTimer?.cancel();
}
_beginQueryState() {
_updateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
_updateState();
});
}
_updateState() {
bind.mainAccountAuthResult().then((result) {
if (result.isEmpty) {
return;
}
final resultMap = jsonDecode(result);
if (resultMap == null) {
return;
}
final String stateMsg = resultMap['state_msg'];
String failedMsg = resultMap['failed_msg'];
final String? url = resultMap['url'];
final authBody = resultMap['auth_body'];
if (_stateMsg != stateMsg || _FailedMsg != failedMsg) {
if (_url.isEmpty && url != null && url.isNotEmpty) {
launchUrl(Uri.parse(url));
_url = url;
}
if (authBody != null) {
_updateTimer?.cancel();
final String username = authBody['user']['name'];
widget.curOP.value = '';
widget.cbLogin(username);
}
setState(() {
_stateMsg = stateMsg;
_FailedMsg = failedMsg;
if (failedMsg.isNotEmpty) {
widget.curOP.value = '';
_updateTimer?.cancel();
}
});
}
});
}
_resetState() {
_stateMsg = '';
_FailedMsg = '';
_url = '';
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ButtonOP(
op: widget.config.op,
curOP: widget.curOP,
iconWidth: widget.config.iconWidth,
primaryColor: str2color(widget.config.op, 0x7f),
height: 36,
onTap: () async {
_resetState();
widget.curOP.value = widget.config.op;
await bind.mainAccountAuth(op: widget.config.op);
_beginQueryState();
},
),
Obx(() {
if (widget.curOP.isNotEmpty &&
widget.curOP.value != widget.config.op) {
_FailedMsg = '';
}
return Offstage(
offstage:
_FailedMsg.isEmpty && widget.curOP.value != widget.config.op,
child: Row(
children: [
Text(
_stateMsg,
style: TextStyle(fontSize: 12),
),
SizedBox(width: 8),
Text(
_FailedMsg,
style: TextStyle(
fontSize: 14,
color: Colors.red,
),
),
],
));
}),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: const SizedBox(
height: 5.0,
),
),
),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 20),
child: ElevatedButton(
onPressed: () {
widget.curOP.value = '';
_updateTimer?.cancel();
_resetState();
bind.mainAccountAuthCancel();
},
child: Text(
translate('Cancel'),
style: TextStyle(fontSize: 15),
),
),
),
),
),
],
);
}
}
class LoginWidgetOP extends StatelessWidget {
final List<ConfigOP> ops;
final RxString curOP;
final Function(String) cbLogin;
LoginWidgetOP({
Key? key,
required this.ops,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var children = ops
.map((op) => [
WidgetOP(
config: op,
curOP: curOP,
cbLogin: cbLogin,
),
const Divider(
indent: 5,
endIndent: 5,
)
])
.expand((i) => i)
.toList();
if (children.isNotEmpty) {
children.removeLast();
}
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
));
}
}
class LoginWidgetUserPass extends StatelessWidget {
final String username;
final String pass;
final String usernameMsg;
final String passMsg;
final bool isInProgress;
final RxString curOP;
final Function(String, String) onLogin;
const LoginWidgetUserPass({
Key? key,
required this.username,
required this.pass,
required this.usernameMsg,
required this.passMsg,
required this.isInProgress,
required this.curOP,
required this.onLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var userController = TextEditingController(text: username);
var pwdController = TextEditingController(text: pass);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
),
Container(
padding: kMidButtonPadding,
child: Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: Text(
'${translate("Username")}:',
textAlign: TextAlign.start,
).marginOnly(bottom: 16.0)),
const SizedBox(
width: 24.0,
),
Expanded(
child: TextField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: usernameMsg.isNotEmpty ? usernameMsg : null),
controller: userController,
focusNode: FocusNode()..requestFocus(),
),
),
],
),
),
const SizedBox(
height: 8.0,
),
Container(
padding: kMidButtonPadding,
child: Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: Text('${translate("Password")}:')
.marginOnly(bottom: 16.0)),
const SizedBox(
width: 24.0,
),
Expanded(
child: TextField(
obscureText: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: passMsg.isNotEmpty ? passMsg : null),
controller: pwdController,
),
),
],
),
),
const SizedBox(
height: 4.0,
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator()),
const SizedBox(
height: 12.0,
),
Row(children: [
Expanded(
child: Container(
height: 38,
padding: kMidButtonPadding,
child: Obx(() => ElevatedButton(
style: curOP.value.isEmpty || curOP.value == 'rustdesk'
? null
: ElevatedButton.styleFrom(
primary: Colors.grey,
),
child: Text(
translate('Login'),
style: TextStyle(fontSize: 16),
),
onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk'
? () {
onLogin(userController.text, pwdController.text);
}
: null,
)),
),
),
]),
],
);
}
}
/// common login dialog for desktop
/// call this directly
Future<bool> loginDialog() async {
String username = '';
var usernameMsg = '';
String pass = '';
var passMsg = '';
var isInProgress = false;
var completer = Completer<bool>();
final RxString curOP = ''.obs;
gFFI.dialogManager.show((setState, close) {
cancel() {
isInProgress = false;
completer.complete(false);
close();
}
onLogin(String username0, String pass0) async {
setState(() {
usernameMsg = '';
passMsg = '';
isInProgress = true;
});
cancel() {
curOP.value = '';
if (isInProgress) {
setState(() {
isInProgress = false;
});
}
}
curOP.value = 'rustdesk';
username = username0;
pass = pass0;
if (username.isEmpty) {
usernameMsg = translate('Username missed');
cancel();
return;
}
if (pass.isEmpty) {
passMsg = translate('Password missed');
cancel();
return;
}
try {
final resp = await gFFI.userModel.login(username, pass);
if (resp.containsKey('error')) {
passMsg = resp['error'];
cancel();
return;
}
// {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w,
// token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}}
debugPrint('$resp');
completer.complete(true);
} catch (err) {
debugPrintStack(label: err.toString());
cancel();
return;
}
close();
}
return CustomAlertDialog(
title: Text(translate('Login')),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
),
LoginWidgetUserPass(
username: username,
pass: pass,
usernameMsg: usernameMsg,
passMsg: passMsg,
isInProgress: isInProgress,
curOP: curOP,
onLogin: onLogin,
),
const SizedBox(
height: 8.0,
),
Center(
child: Text(
translate('or'),
style: TextStyle(fontSize: 16),
)),
const SizedBox(
height: 8.0,
),
LoginWidgetOP(
ops: [
ConfigOP(op: 'Github', iconWidth: 20),
ConfigOP(op: 'Google', iconWidth: 20),
ConfigOP(op: 'Okta', iconWidth: 38),
],
curOP: curOP,
cbLogin: (String username) {
gFFI.userModel.userName.value = username;
completer.complete(true);
close();
},
),
],
),
),
actions: [msgBoxButton(translate('Close'), cancel)],
onCancel: cancel,
);
});
return completer.future;
}

View File

@@ -118,6 +118,15 @@ abstract class MenuEntryBase<T> {
this.enabled,
});
List<mod_menu.PopupMenuEntry<T>> build(BuildContext context, MenuConfig conf);
enabledStyle(BuildContext context) => TextStyle(
color: Theme.of(context).textTheme.titleLarge?.color,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
disabledStyle() => TextStyle(
color: Colors.grey,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
}
class MenuEntryDivider<T> extends MenuEntryBase<T> {
@@ -189,54 +198,76 @@ class MenuEntryRadios<T> extends MenuEntryBase<T> {
mod_menu.PopupMenuEntry<T> _buildMenuItem(
BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) {
Widget getTextChild() {
final enabledTextChild = Text(
opt.text,
style: enabledStyle(context),
);
final disabledTextChild = Text(
opt.text,
style: disabledStyle(),
);
if (opt.enabled == null) {
return enabledTextChild;
} else {
return Obx(
() => opt.enabled!.isTrue ? enabledTextChild : disabledTextChild);
}
}
final child = Container(
padding: padding,
alignment: AlignmentDirectional.centerStart,
constraints:
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
child: Row(
children: [
getTextChild(),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Transform.scale(
scale: MenuConfig.iconScale,
child: Obx(() => opt.value == curOption.value
? IconButton(
padding:
const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0),
hoverColor: Colors.transparent,
focusColor: Colors.transparent,
onPressed: () {},
icon: Icon(
Icons.check,
color: (opt.enabled ?? true.obs).isTrue
? conf.commonColor
: Colors.grey,
))
: const SizedBox.shrink()),
))),
],
),
);
onPressed() {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
setOption(opt.value);
}
return mod_menu.PopupMenuItem(
padding: EdgeInsets.zero,
height: conf.height,
child: Container(
width: conf.boxWidth,
child: TextButton(
child: Container(
padding: padding,
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(
minHeight: conf.height, maxHeight: conf.height),
child: Row(
children: [
Text(
opt.text,
style: TextStyle(
color: Theme.of(context).textTheme.titleLarge?.color,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Transform.scale(
scale: MenuConfig.iconScale,
child: Obx(() => opt.value == curOption.value
? IconButton(
padding: const EdgeInsets.fromLTRB(
8.0, 0.0, 8.0, 0.0),
hoverColor: Colors.transparent,
focusColor: Colors.transparent,
onPressed: () {},
icon: Icon(
Icons.check,
color: conf.commonColor,
))
: const SizedBox.shrink()),
))),
],
),
),
onPressed: () {
if (opt.dismissOnClicked && Navigator.canPop(context)) {
Navigator.pop(context);
}
setOption(opt.value);
},
)),
width: conf.boxWidth,
child: opt.enabled == null
? TextButton(
child: child,
onPressed: onPressed,
)
: Obx(() => TextButton(
child: child,
onPressed: opt.enabled!.isTrue ? onPressed : null,
)),
),
);
}
@@ -567,12 +598,9 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
const SizedBox(width: MenuConfig.midPadding),
Obx(() => Text(
text,
style: TextStyle(
color: super.enabled!.value
? Theme.of(context).textTheme.titleLarge?.color
: Colors.grey,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal),
style: super.enabled!.value
? enabledStyle(context)
: disabledStyle(),
)),
Expanded(
child: Align(
@@ -605,14 +633,6 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
);
Widget _buildChild(BuildContext context, MenuConfig conf) {
final enabledStyle = TextStyle(
color: Theme.of(context).textTheme.titleLarge?.color,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
const disabledStyle = TextStyle(
color: Colors.grey,
fontSize: MenuConfig.fontSize,
fontWeight: FontWeight.normal);
super.enabled ??= true.obs;
return Obx(() => Container(
width: conf.boxWidth,
@@ -631,7 +651,7 @@ class MenuEntryButton<T> extends MenuEntryBase<T> {
constraints:
BoxConstraints(minHeight: conf.height, maxHeight: conf.height),
child: childBuilder(
super.enabled!.value ? enabledStyle : disabledStyle),
super.enabled!.value ? enabledStyle(context) : disabledStyle()),
),
)));
}

View File

@@ -22,6 +22,7 @@ import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import './popup_menu.dart';
import './material_mod_popup_menu.dart' as mod_menu;
import './kb_layout_type_chooser.dart';
class MenubarState {
final kStoreKey = 'remoteMenubarState';
@@ -171,6 +172,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
@override
Widget build(BuildContext context) {
// No need to use future builder here.
_updateScreen();
return Align(
alignment: Alignment.topCenter,
child: Obx(() => show.value
@@ -362,8 +365,6 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
RxInt display = CurrentDisplayState.find(widget.id);
if (display.value != i) {
bind.sessionSwitchDisplay(id: widget.id, value: i);
pi.currentDisplay = i;
display.value = i;
}
},
)
@@ -569,7 +570,8 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
),
]);
// {handler.get_audit_server() && <li #note>{translate('Note')}</li>}
final auditServer = bind.sessionGetAuditServerSync(id: widget.id);
final auditServer =
bind.sessionGetAuditServerSync(id: widget.id, typ: "conn");
if (auditServer.isNotEmpty) {
displayMenu.add(
MenuEntryButton<String>(
@@ -587,7 +589,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
displayMenu.add(MenuEntryDivider());
if (perms['keyboard'] != false) {
if (pi.platform == 'Linux' || pi.sasEnabled) {
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
'${translate("Insert")} Ctrl + Alt + Del',
@@ -602,9 +604,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
}
if (perms['restart'] != false &&
(pi.platform == 'Linux' ||
pi.platform == 'Windows' ||
pi.platform == 'Mac OS')) {
(pi.platform == kPeerPlatformLinux ||
pi.platform == kPeerPlatformWindows ||
pi.platform == kPeerPlatformMacOS)) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
translate('Restart Remote Device'),
@@ -631,7 +633,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
dismissOnClicked: true,
));
if (pi.platform == 'Windows') {
if (pi.platform == kPeerPlatformWindows) {
displayMenu.add(MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Obx(() => Text(
translate(
@@ -697,12 +699,12 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
if (_screen == null) {
return false;
}
double scale = _screen!.scaleFactor;
double selfWidth = _screen!.frame.width;
double selfHeight = _screen!.frame.height;
final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor;
double selfWidth = _screen!.visibleFrame.width;
double selfHeight = _screen!.visibleFrame.height;
if (isFullscreen) {
selfWidth = _screen!.visibleFrame.width;
selfHeight = _screen!.visibleFrame.height;
selfWidth = _screen!.frame.width;
selfHeight = _screen!.frame.height;
}
final canvasModel = widget.ffi.canvasModel;
@@ -827,7 +829,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
qualityInitValue = qualityMaxValue;
}
final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
final debouncerQuanlity = Debouncer<double>(
final debouncerQuality = Debouncer<double>(
Duration(milliseconds: 1000),
onChanged: (double v) {
setCustomValues(quality: v);
@@ -843,7 +845,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
divisions: 90,
onChanged: (double value) {
qualitySliderValue.value = value;
debouncerQuanlity.value = value;
debouncerQuality.value = value;
},
),
SizedBox(
@@ -934,11 +936,13 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
text: translate('ScrollAuto'),
value: kRemoteScrollStyleAuto,
dismissOnClicked: true,
enabled: widget.ffi.canvasModel.imageOverflow,
),
MenuEntryRadioOption(
text: translate('Scrollbar'),
value: kRemoteScrollStyleBar,
dismissOnClicked: true,
enabled: widget.ffi.canvasModel.imageOverflow,
),
],
curOptionGetter: () async =>
@@ -952,75 +956,77 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
dismissOnClicked: true,
));
displayMenu.insert(3, MenuEntryDivider<String>());
}
if (_isWindowCanBeAdjusted(remoteCount)) {
displayMenu.insert(
0,
MenuEntryDivider<String>(),
);
displayMenu.insert(
0,
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Container(
child: Text(
translate('Adjust Window'),
style: style,
)),
proc: () {
() async {
await _updateScreen();
if (_screen != null) {
_setFullscreen(false);
double scale = _screen!.scaleFactor;
final wndRect =
await WindowController.fromWindowId(windowId).getFrame();
final mediaSize = MediaQueryData.fromWindow(ui.window).size;
// On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
// https://stackoverflow.com/a/7561083
double magicWidth =
wndRect.right - wndRect.left - mediaSize.width * scale;
double magicHeight =
wndRect.bottom - wndRect.top - mediaSize.height * scale;
if (_isWindowCanBeAdjusted(remoteCount)) {
displayMenu.insert(
0,
MenuEntryDivider<String>(),
);
displayMenu.insert(
0,
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Container(
child: Text(
translate('Adjust Window'),
style: style,
)),
proc: () {
() async {
await _updateScreen();
if (_screen != null) {
_setFullscreen(false);
double scale = _screen!.scaleFactor;
final wndRect =
await WindowController.fromWindowId(windowId).getFrame();
final mediaSize = MediaQueryData.fromWindow(ui.window).size;
// On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
// https://stackoverflow.com/a/7561083
double magicWidth =
wndRect.right - wndRect.left - mediaSize.width * scale;
double magicHeight =
wndRect.bottom - wndRect.top - mediaSize.height * scale;
final canvasModel = widget.ffi.canvasModel;
final width = (canvasModel.getDisplayWidth() +
canvasModel.windowBorderWidth * 2) *
scale +
magicWidth;
final height = (canvasModel.getDisplayHeight() +
canvasModel.tabBarHeight +
canvasModel.windowBorderWidth * 2) *
scale +
magicHeight;
double left = wndRect.left + (wndRect.width - width) / 2;
double top = wndRect.top + (wndRect.height - height) / 2;
final canvasModel = widget.ffi.canvasModel;
final width =
(canvasModel.getDisplayWidth() * canvasModel.scale +
canvasModel.windowBorderWidth * 2) *
scale +
magicWidth;
final height =
(canvasModel.getDisplayHeight() * canvasModel.scale +
canvasModel.tabBarHeight +
canvasModel.windowBorderWidth * 2) *
scale +
magicHeight;
double left = wndRect.left + (wndRect.width - width) / 2;
double top = wndRect.top + (wndRect.height - height) / 2;
Rect frameRect = _screen!.frame;
if (!isFullscreen) {
frameRect = _screen!.visibleFrame;
Rect frameRect = _screen!.frame;
if (!isFullscreen) {
frameRect = _screen!.visibleFrame;
}
if (left < frameRect.left) {
left = frameRect.left;
}
if (top < frameRect.top) {
top = frameRect.top;
}
if ((left + width) > frameRect.right) {
left = frameRect.right - width;
}
if ((top + height) > frameRect.bottom) {
top = frameRect.bottom - height;
}
await WindowController.fromWindowId(windowId)
.setFrame(Rect.fromLTWH(left, top, width, height));
}
if (left < frameRect.left) {
left = frameRect.left;
}
if (top < frameRect.top) {
top = frameRect.top;
}
if ((left + width) > frameRect.right) {
left = frameRect.right - width;
}
if ((top + height) > frameRect.bottom) {
top = frameRect.bottom - height;
}
await WindowController.fromWindowId(windowId)
.setFrame(Rect.fromLTWH(left, top, width, height));
}
}();
},
padding: padding,
dismissOnClicked: true,
),
);
}();
},
padding: padding,
dismissOnClicked: true,
),
);
}
}
/// Show Codec Preference
@@ -1032,7 +1038,9 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
final h265 = codecsJson['h265'] ?? false;
codecs.add(h264);
codecs.add(h265);
} finally {}
} catch (e) {
debugPrint("Show Codec Preference err=$e");
}
if (codecs.length == 2 && (codecs[0] || codecs[1])) {
displayMenu.add(MenuEntryRadios<String>(
text: translate('Codec Preference'),
@@ -1082,7 +1090,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
/// Show remote cursor
if (!widget.ffi.canvasModel.cursorEmbeded) {
if (!widget.ffi.canvasModel.cursorEmbedded) {
displayMenu.add(() {
final state = ShowRemoteCursorState.find(widget.id);
return MenuEntrySwitch2<String>(
@@ -1149,7 +1157,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
if (Platform.isWindows &&
pi.platform == 'Windows' &&
pi.platform == kPeerPlatformWindows &&
perms['file'] != false) {
displayMenu.add(_createSwitchMenuEntry(
'Allow file copy and paste', 'enable-file-transfer', padding, true));
@@ -1182,7 +1190,7 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
}
List<MenuEntryBase<String>> _getKeyboardMenu() {
final keyboardMenu = [
final List<MenuEntryBase<String>> keyboardMenu = [
MenuEntryRadios<String>(
text: translate('Ratio'),
optionsGetter: () {
@@ -1209,11 +1217,58 @@ class _RemoteMenubarState extends State<RemoteMenubar> {
},
optionSetter: (String oldValue, String newValue) async {
await bind.sessionSetKeyboardMode(id: widget.id, value: newValue);
widget.ffi.canvasModel.updateViewStyle();
},
)
];
final localPlatform =
getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform);
if (localPlatform != '') {
keyboardMenu.add(MenuEntryDivider());
keyboardMenu.add(
MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Container(
alignment: AlignmentDirectional.center,
height: _MenubarTheme.height,
child: Row(
children: [
Obx(() => RichText(
text: TextSpan(
text: '${translate('Local keyboard type')}: ',
style: DefaultTextStyle.of(context).style,
children: <TextSpan>[
TextSpan(
text: KBLayoutType.value,
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
)),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Transform.scale(
scale: 0.8,
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(Icons.settings),
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
showKBLayoutTypeChooser(
localPlatform, widget.ffi.dialogManager);
},
),
),
))
],
)),
proc: () {},
padding: EdgeInsets.zero,
dismissOnClicked: false,
),
);
}
return keyboardMenu;
}
@@ -1373,10 +1428,10 @@ class _DraggableShowHide extends StatefulWidget {
}) : super(key: key);
@override
State<_DraggableShowHide> createState() => __DraggableShowHideState();
State<_DraggableShowHide> createState() => _DraggableShowHideState();
}
class __DraggableShowHideState extends State<_DraggableShowHide> {
class _DraggableShowHideState extends State<_DraggableShowHide> {
Offset position = Offset.zero;
Size size = Size.zero;
@@ -1385,7 +1440,8 @@ class __DraggableShowHideState extends State<_DraggableShowHide> {
axis: Axis.horizontal,
child: Icon(
Icons.drag_indicator,
size: 15,
size: 20,
color: Colors.grey,
),
feedback: widget,
onDragStarted: (() {
@@ -1428,7 +1484,7 @@ class __DraggableShowHideState extends State<_DraggableShowHide> {
}),
child: Obx((() => Icon(
widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
size: 15,
size: 20,
))),
),
],
@@ -1441,7 +1497,7 @@ class __DraggableShowHideState extends State<_DraggableShowHide> {
border: Border.all(color: MyTheme.border),
),
child: SizedBox(
height: 15,
height: 20,
child: child,
),
),

View File

@@ -331,6 +331,7 @@ class DesktopTab extends StatelessWidget {
return _buildBlock(
child: Obx(() => PageView(
controller: state.value.pageController,
physics: NeverScrollableScrollPhysics(),
children: state.value.tabs
.map((tab) => tab.page)
.toList(growable: false))));
@@ -526,13 +527,19 @@ class WindowActionPanelState extends State<WindowActionPanel>
void onWindowClose() async {
// hide window on close
if (widget.isMainWindow) {
await rustDeskWinManager.unregisterActiveWindow(0);
// `hide` must be placed after unregisterActiveWindow, because once all windows are hidden,
// flutter closes the application on macOS. We should ensure the post-run logic has ran successfully.
// e.g.: saving window position.
await windowManager.hide();
rustDeskWinManager.unregisterActiveWindow(0);
} else {
widget.onClose?.call();
// it's safe to hide the subwindow
await WindowController.fromWindowId(windowId!).hide();
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": windowId!});
await Future.wait([
rustDeskWinManager
.call(WindowType.Main, kWindowEventHide, {"id": windowId!}),
widget.onClose?.call() ?? Future.microtask(() => null)
]);
}
super.onWindowClose();
}
@@ -899,7 +906,7 @@ class _TabState extends State<_Tab> with RestorationMixin {
children: [
_buildTabContent(),
Obx((() => _CloseButton(
visiable: hover.value && widget.closable,
visible: hover.value && widget.closable,
tabSelected: isSelected,
onClose: () => widget.onClose(),
)))
@@ -931,13 +938,13 @@ class _TabState extends State<_Tab> with RestorationMixin {
}
class _CloseButton extends StatelessWidget {
final bool visiable;
final bool visible;
final bool tabSelected;
final Function onClose;
const _CloseButton({
Key? key,
required this.visiable,
required this.visible,
required this.tabSelected,
required this.onClose,
}) : super(key: key);
@@ -947,7 +954,7 @@ class _CloseButton extends StatelessWidget {
return SizedBox(
width: _kIconSize,
child: Offstage(
offstage: !visiable,
offstage: !visible,
child: InkWell(
customBorder: const RoundedRectangleBorder(),
onTap: () => onClose(),

View File

@@ -61,6 +61,8 @@ Future<void> main(List<String> args) async {
kAppTypeDesktopRemote,
'RustDesk - Remote Desktop',
);
WindowController.fromWindowId(windowId!)
.setTitle('RustDesk - Remote Desktop');
break;
case WindowType.FileTransfer:
desktopType = DesktopType.fileTransfer;
@@ -69,6 +71,8 @@ Future<void> main(List<String> args) async {
kAppTypeDesktopFileTransfer,
'RustDesk - File Transfer',
);
WindowController.fromWindowId(windowId!)
.setTitle('RustDesk - File Transfer');
break;
case WindowType.PortForward:
desktopType = DesktopType.portForward;
@@ -117,6 +121,7 @@ void runMainApp(bool startService) async {
// await windowManager.ensureInitialized();
gFFI.serverModel.startService();
}
gFFI.userModel.refreshCurrentUser();
runApp(App());
// restore the location of the main window before window hide or show
await restoreWindowPosition(WindowType.Main);
@@ -134,6 +139,7 @@ void runMainApp(bool startService) async {
windowManager.waitUntilReadyToShow(windowOptions, () async {
windowManager.setOpacity(1);
});
windowManager.setTitle("RustDesk");
}
void runMobileApp() async {
@@ -195,6 +201,9 @@ void runMultiWindow(
// no such appType
exit(0);
}
// show window from hidden status
WindowController.fromWindowId(windowId!).show();
WindowController.fromWindowId(windowId!).setTitle(title);
}
void runConnectionManagerScreen(bool hide) async {

View File

@@ -7,9 +7,8 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
import '../../common/widgets/address_book.dart';
import '../../common/widgets/login.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/peers_view.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
@@ -258,7 +257,7 @@ class _WebMenuState extends State<WebMenu> {
}
if (value == 'login') {
if (gFFI.userModel.userName.value.isEmpty) {
showLogin(gFFI.dialogManager);
loginDialog();
} else {
gFFI.userModel.logOut();
}

View File

@@ -518,7 +518,7 @@ class _RemotePageState extends State<RemotePage> {
),
),
];
if (!gFFI.canvasModel.cursorEmbeded) {
if (!gFFI.canvasModel.cursorEmbedded) {
paints.add(CursorPaint());
}
return paints;
@@ -527,7 +527,7 @@ class _RemotePageState extends State<RemotePage> {
Widget getBodyForDesktopWithListener(bool keyboard) {
var paints = <Widget>[ImagePaint()];
if (!gFFI.canvasModel.cursorEmbeded) {
if (!gFFI.canvasModel.cursorEmbedded) {
final cursor = bind.sessionGetToggleOptionSync(
id: widget.id, arg: 'show-remote-cursor');
if (keyboard || cursor) {
@@ -574,14 +574,14 @@ class _RemotePageState extends State<RemotePage> {
more.add(PopupMenuItem<String>(
child: Text(translate('Physical Keyboard Input Mode')),
value: 'input-mode'));
if (pi.platform == 'Linux' || pi.sasEnabled) {
if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
more.add(PopupMenuItem<String>(
child: Text('${translate('Insert')} Ctrl + Alt + Del'),
value: 'cad'));
}
more.add(PopupMenuItem<String>(
child: Text(translate('Insert Lock')), value: 'lock'));
if (pi.platform == 'Windows' &&
if (pi.platform == kPeerPlatformWindows &&
await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') !=
true) {
more.add(PopupMenuItem<String>(
@@ -591,9 +591,9 @@ class _RemotePageState extends State<RemotePage> {
}
}
if (perms["restart"] != false &&
(pi.platform == "Linux" ||
pi.platform == "Windows" ||
pi.platform == "Mac OS")) {
(pi.platform == kPeerPlatformLinux ||
pi.platform == kPeerPlatformWindows ||
pi.platform == kPeerPlatformMacOS)) {
more.add(PopupMenuItem<String>(
child: Text(translate('Restart Remote Device')), value: 'restart'));
}
@@ -696,7 +696,7 @@ class _RemotePageState extends State<RemotePage> {
gFFI.dialogManager.show((setState, close) {
void setMode(String? v) async {
await bind.sessionPeerOption(
id: widget.id, name: "keyboard-mode", value: v ?? "");
id: widget.id, name: "keyboard-mode", value: v ?? "");
setState(() => current = v ?? '');
Future.delayed(Duration(milliseconds: 300), close);
}
@@ -740,7 +740,7 @@ class _RemotePageState extends State<RemotePage> {
}
final pi = gFFI.ffiModel.pi;
final isMac = pi.platform == "Mac OS";
final isMac = pi.platform == kPeerPlatformMacOS;
final modifiers = <Widget>[
wrap('Ctrl ', () {
setState(() => inputModel.ctrl = !inputModel.ctrl);
@@ -978,7 +978,9 @@ void showOptions(
final h265 = codecsJson['h265'] ?? false;
codecs.add(h264);
codecs.add(h265);
} finally {}
} catch (e) {
debugPrint("Show Codec Preference err=$e");
}
}
dialogManager.show((setState, close) {
@@ -993,7 +995,7 @@ void showOptions(
}
more.add(getToggle(
id, setState, 'lock-after-session-end', 'Lock after session end'));
if (pi.platform == 'Windows') {
if (pi.platform == kPeerPlatformWindows) {
more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode'));
}
}
@@ -1056,7 +1058,7 @@ void showOptions(
final toggles = [
getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'),
];
if (!gFFI.canvasModel.cursorEmbeded) {
if (!gFFI.canvasModel.cursorEmbedded) {
toggles.insert(0,
getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor'));
}

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
@@ -9,11 +8,11 @@ import 'package:qr_code_scanner/qr_code_scanner.dart';
import 'package:zxing2/qrcode.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
import '../widgets/dialog.dart';
class ScanPage extends StatefulWidget {
@override
_ScanPageState createState() => _ScanPageState();
State<ScanPage> createState() => _ScanPageState();
}
class _ScanPageState extends State<ScanPage> {
@@ -42,9 +41,9 @@ class _ScanPageState extends State<ScanPage> {
icon: Icon(Icons.image_search),
iconSize: 32.0,
onPressed: () async {
final ImagePicker _picker = ImagePicker();
final ImagePicker picker = ImagePicker();
final XFile? file =
await _picker.pickImage(source: ImageSource.gallery);
await picker.pickImage(source: ImageSource.gallery);
if (file != null) {
var image = img.decodeNamedImage(
File(file.path).readAsBytesSync(), file.path)!;
@@ -139,155 +138,12 @@ class _ScanPageState extends State<ScanPage> {
return;
}
try {
Map<String, dynamic> values = json.decode(data.substring(7));
var host = values['host'] != null ? values['host'] as String : '';
var key = values['key'] != null ? values['key'] as String : '';
var api = values['api'] != null ? values['api'] as String : '';
final sc = ServerConfig.decode(data.substring(7));
Timer(Duration(milliseconds: 60), () {
showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager);
showServerSettingsWithValue(sc, gFFI.dialogManager);
});
} catch (e) {
showToast('Invalid QR code');
}
}
}
void showServerSettingsWithValue(String id, String relay, String key,
String api, OverlayDialogManager dialogManager) async {
Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
String id0 = oldOptions['custom-rendezvous-server'] ?? "";
String relay0 = oldOptions['relay-server'] ?? "";
String api0 = oldOptions['api-server'] ?? "";
String key0 = oldOptions['key'] ?? "";
var isInProgress = false;
final idController = TextEditingController(text: id);
final relayController = TextEditingController(text: relay);
final apiController = TextEditingController(text: api);
String? idServerMsg;
String? relayServerMsg;
String? apiServerMsg;
dialogManager.show((setState, close) {
Future<bool> validate() async {
if (idController.text != id) {
final res = await validateAsync(idController.text);
setState(() => idServerMsg = res);
if (idServerMsg != null) return false;
id = idController.text;
}
if (relayController.text != relay) {
relayServerMsg = await validateAsync(relayController.text);
if (relayServerMsg != null) return false;
relay = relayController.text;
}
if (apiController.text != relay) {
apiServerMsg = await validateAsync(apiController.text);
if (apiServerMsg != null) return false;
api = apiController.text;
}
return true;
}
return CustomAlertDialog(
title: Text(translate('ID/Relay Server')),
content: Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
controller: idController,
decoration: InputDecoration(
labelText: translate('ID Server'),
errorText: idServerMsg),
)
] +
(isAndroid
? [
TextFormField(
controller: relayController,
decoration: InputDecoration(
labelText: translate('Relay Server'),
errorText: relayServerMsg),
)
]
: []) +
[
TextFormField(
controller: apiController,
decoration: InputDecoration(
labelText: translate('API Server'),
),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (v) {
if (v != null && v.length > 0) {
if (!(v.startsWith('http://') ||
v.startsWith("https://"))) {
return translate("invalid_http");
}
}
return apiServerMsg;
},
),
TextFormField(
initialValue: key,
decoration: InputDecoration(
labelText: 'Key',
),
onChanged: (String? value) {
if (value != null) key = value.trim();
},
),
Offstage(
offstage: !isInProgress,
child: LinearProgressIndicator())
])),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () {
close();
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: () async {
setState(() {
idServerMsg = null;
relayServerMsg = null;
apiServerMsg = null;
isInProgress = true;
});
if (await validate()) {
if (id != id0) {
bind.mainSetOption(key: "custom-rendezvous-server", value: id);
}
if (relay != relay0) {
bind.mainSetOption(key: "relay-server", value: relay);
}
if (key != key0) bind.mainSetOption(key: "key", value: key);
if (api != api0) {
bind.mainSetOption(key: "api-server", value: api);
}
close();
}
setState(() {
isInProgress = false;
});
},
child: Text(translate('OK')),
),
],
);
});
}
Future<String?> validateAsync(String value) async {
value = value.trim();
if (value.isEmpty) {
return null;
}
final res = await bind.mainTestIfValidServer(server: value);
return res.isEmpty ? null : res;
}

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
import 'package:provider/provider.dart';
@@ -107,12 +109,23 @@ class ServerPage extends StatefulWidget implements PageShape {
}
class _ServerPageState extends State<ServerPage> {
Timer? _updateTimer;
@override
void initState() {
super.initState();
_updateTimer = periodic_immediate(const Duration(seconds: 3), () async {
await gFFI.serverModel.fetchID();
});
gFFI.serverModel.checkAndroidPermission();
}
@override
void dispose() {
_updateTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
checkService();

View File

@@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../widgets/dialog.dart';
@@ -300,7 +301,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
leading: Icon(Icons.person),
onPressed: (context) {
if (gFFI.userModel.userName.value.isEmpty) {
showLogin(gFFI.dialogManager);
loginDialog();
} else {
gFFI.userModel.logOut();
}
@@ -391,17 +392,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
void showServerSettings(OverlayDialogManager dialogManager) async {
Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
String id = options['custom-rendezvous-server'] ?? "";
String relay = options['relay-server'] ?? "";
String api = options['api-server'] ?? "";
String key = options['key'] ?? "";
showServerSettingsWithValue(id, relay, key, api, dialogManager);
showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
}
void showLanguageSettings(OverlayDialogManager dialogManager) async {
try {
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
var lang = await bind.mainGetLocalOption(key: "lang");
var lang = bind.mainGetLocalOption(key: "lang");
dialogManager.show((setState, close) {
setLang(v) {
if (lang != v) {
@@ -486,78 +483,6 @@ void showAbout(OverlayDialogManager dialogManager) {
}, clickMaskDismiss: true, backDismiss: true);
}
void showLogin(OverlayDialogManager dialogManager) {
final passwordController = TextEditingController();
final nameController = TextEditingController();
var loading = false;
var error = '';
dialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('Login')),
content: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(
autofocus: true,
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
labelText: translate('Username'),
),
controller: nameController,
),
PasswordWidget(controller: passwordController, autoFocus: false),
]),
actions: (loading
? <Widget>[CircularProgressIndicator()]
: (error != ""
? <Widget>[
Text(translate(error),
style: TextStyle(color: Colors.red))
]
: <Widget>[])) +
<Widget>[
TextButton(
style: flatButtonStyle,
onPressed: loading
? null
: () {
close();
setState(() {
loading = false;
});
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: loading
? null
: () async {
final name = nameController.text.trim();
final pass = passwordController.text.trim();
if (name != "" && pass != "") {
setState(() {
loading = true;
});
final resp = await gFFI.userModel.login(name, pass);
setState(() {
loading = false;
});
if (resp.containsKey('error')) {
error = resp['error'];
return;
}
gFFI.abModel.pullAb();
}
close();
},
child: Text(translate('OK')),
),
],
);
});
}
class ScanButton extends StatelessWidget {
@override
Widget build(BuildContext context) {

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import '../../common.dart';
@@ -236,6 +237,145 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) {
]));
}
void showServerSettingsWithValue(
ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
Map<String, dynamic> 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;
dialogManager.show((setState, close) {
Future<bool> 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;
}
return CustomAlertDialog(
title: Text(translate('ID/Relay Server')),
content: Form(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
controller: idCtrl,
decoration: InputDecoration(
labelText: translate('ID Server'),
errorText: idServerMsg),
)
] +
(isAndroid
? [
TextFormField(
controller: relayCtrl,
decoration: InputDecoration(
labelText: translate('Relay Server'),
errorText: relayServerMsg),
)
]
: []) +
[
TextFormField(
controller: apiCtrl,
decoration: InputDecoration(
labelText: translate('API Server'),
),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (v) {
if (v != null && v.isNotEmpty) {
if (!(v.startsWith('http://') ||
v.startsWith("https://"))) {
return translate("invalid_http");
}
}
return apiServerMsg;
},
),
TextFormField(
controller: keyCtrl,
decoration: InputDecoration(
labelText: 'Key',
),
),
Offstage(
offstage: !isInProgress,
child: LinearProgressIndicator())
])),
actions: [
TextButton(
style: flatButtonStyle,
onPressed: () {
close();
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: () async {
setState(() {
idServerMsg = null;
relayServerMsg = null;
apiServerMsg = null;
isInProgress = true;
});
if (await validate()) {
if (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);
}
close();
showToast(translate('Successful'));
}
setState(() {
isInProgress = false;
});
},
child: Text(translate('OK')),
),
],
);
});
}
Future<String?> validateAsync(String value) async {
value = value.trim();
if (value.isEmpty) {
return null;
}
final res = await bind.mainTestIfValidServer(server: value);
return res.isEmpty ? null : res;
}
class PasswordWidget extends StatefulWidget {
PasswordWidget({Key? key, required this.controller, this.autoFocus = true})
: super(key: key);
@@ -285,7 +425,7 @@ class _PasswordWidgetState extends State<PasswordWidget> {
color: Theme.of(context).primaryColorDark,
),
onPressed: () {
// Update the state i.e. toogle the state of passwordVisible variable
// Update the state i.e. toggle the state of passwordVisible variable
setState(() {
_passwordVisible = !_passwordVisible;
});

View File

@@ -21,16 +21,13 @@ class AbModel {
AbModel(this.parent);
FFI? get _ffi => parent.target;
Future<dynamic> pullAb() async {
if (_ffi!.userModel.userName.isEmpty) return;
if (gFFI.userModel.userName.isEmpty) return;
abLoading.value = true;
abError.value = "";
final api = "${await bind.mainGetApiServer()}/api/ab/get";
try {
final resp =
await http.post(Uri.parse(api), headers: await getHttpHeaders());
final resp = await http.post(Uri.parse(api), headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) {
@@ -63,7 +60,8 @@ class AbModel {
return null;
}
void reset() {
Future<void> reset() async {
await bind.mainSetLocalOption(key: "selected-tags", value: '');
tags.clear();
peers.clear();
}
@@ -103,7 +101,7 @@ class AbModel {
Future<void> pushAb() async {
abLoading.value = true;
final api = "${await bind.mainGetApiServer()}/api/ab";
var authHeaders = await getHttpHeaders();
var authHeaders = getHttpHeaders();
authHeaders['Content-Type'] = "application/json";
final peersJsonData = peers.map((e) => e.toJson()).toList();
final body = jsonEncode({
@@ -188,9 +186,4 @@ class AbModel {
await pushAb();
}
}
void clear() {
peers.clear();
tags.clear();
}
}

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
@@ -213,7 +214,6 @@ class FileModel extends ChangeNotifier {
}
receiveFileDir(Map<String, dynamic> evt) {
debugPrint("recv file dir:$evt");
if (evt['is_local'] == "false") {
// init remote home, the connection will automatic read remote home when established,
try {
@@ -237,7 +237,9 @@ class FileModel extends ChangeNotifier {
debugPrint("init remote home:${fd.path}");
_currentRemoteDir = fd;
}
} finally {}
} catch (e) {
debugPrint("receiveFileDir err=$e");
}
}
_fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
notifyListeners();
@@ -346,7 +348,7 @@ class FileModel extends ChangeNotifier {
id: parent.target?.id ?? "", name: "remote_show_hidden"))
.isNotEmpty;
_remoteOption.isWindows =
parent.target?.ffiModel.pi.platform.toLowerCase() == "windows";
parent.target?.ffiModel.pi.platform == kPeerPlatformWindows;
await Future.delayed(Duration(milliseconds: 100));

View File

@@ -0,0 +1,140 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
class GroupModel {
final RxBool userLoading = false.obs;
final RxString userLoadError = "".obs;
final RxBool peerLoading = false.obs; //to-do: not used
final RxString peerLoadError = "".obs;
final RxList<UserPayload> users = RxList.empty(growable: true);
final RxList<PeerPayload> peerPayloads = RxList.empty(growable: true);
final RxList<Peer> peersShow = RxList.empty(growable: true);
WeakReference<FFI> parent;
GroupModel(this.parent);
Future<void> reset() async {
userLoading.value = false;
userLoadError.value = "";
peerLoading.value = false;
peerLoadError.value = "";
users.clear();
peerPayloads.clear();
peersShow.clear();
}
Future<void> pull() async {
await reset();
if (gFFI.userModel.userName.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
statePeerTab.check();
return;
}
userLoading.value = true;
userLoadError.value = "";
final api = "${await bind.mainGetApiServer()}/api/users";
try {
var uri0 = Uri.parse(api);
final pageSize = 20;
var total = 0;
int current = 0;
do {
current += 1;
var uri = Uri(
scheme: uri0.scheme,
host: uri0.host,
path: uri0.path,
port: uri0.port,
queryParameters: {
'current': current.toString(),
'pageSize': pageSize.toString(),
if (gFFI.userModel.isAdmin.isFalse)
'grp': gFFI.userModel.groupName.value,
});
final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) {
throw json['error'];
} else {
if (total == 0) total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final user in data) {
users.add(UserPayload.fromJson(user));
}
}
}
}
}
} while (current * pageSize < total);
} catch (err) {
debugPrint('$err');
userLoadError.value = err.toString();
} finally {
userLoading.value = false;
statePeerTab.check();
}
}
Future<void> pullUserPeers(String username) async {
peerPayloads.clear();
peersShow.clear();
peerLoading.value = true;
peerLoadError.value = "";
final api = "${await bind.mainGetApiServer()}/api/peers";
try {
var uri0 = Uri.parse(api);
final pageSize = 20;
var total = 0;
int current = 0;
do {
current += 1;
var uri = Uri(
scheme: uri0.scheme,
host: uri0.host,
path: uri0.path,
port: uri0.port,
queryParameters: {
'current': current.toString(),
'pageSize': pageSize.toString(),
'grp': gFFI.userModel.groupName.value,
'target_user': username
});
final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) {
throw json['error'];
} else {
if (total == 0) total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final p in data) {
final peer = PeerPayload.fromJson(p);
peerPayloads.add(peer);
peersShow.add(PeerPayload.toPeer(peer));
}
}
}
}
}
} while (current * pageSize < total);
} catch (err) {
debugPrint('$err');
peerLoadError.value = err.toString();
} finally {
peerLoading.value = false;
}
}
}

View File

@@ -17,6 +17,10 @@ import './state_model.dart';
/// Mouse button enum.
enum MouseButtons { left, right, wheel }
const _kMouseEventDown = 'mousedown';
const _kMouseEventUp = 'mouseup';
const _kMouseEventMove = 'mousemove';
extension ToString on MouseButtons {
String get value {
switch (this) {
@@ -46,7 +50,7 @@ class InputModel {
// mouse
final isPhysicalMouse = false.obs;
int _lastMouseDownButtons = 0;
int _lastButtons = 0;
Offset lastMousePos = Offset.zero;
get id => parent.target?.id ?? "";
@@ -115,8 +119,11 @@ class InputModel {
keyCode = newData.keyCode;
} else if (e.data is RawKeyEventDataLinux) {
RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux;
scanCode = newData.scanCode;
keyCode = newData.keyCode;
// scanCode and keyCode of RawKeyEventDataLinux are incorrect.
// 1. scanCode means keycode
// 2. keyCode means keysym
scanCode = 0;
keyCode = newData.scanCode;
} else if (e.data is RawKeyEventDataAndroid) {
RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid;
scanCode = newData.scanCode + 8;
@@ -131,16 +138,33 @@ class InputModel {
} else {
down = false;
}
inputRawKey(e.character ?? "", keyCode, scanCode, down);
inputRawKey(e.character ?? '', keyCode, scanCode, down);
}
/// Send raw Key Event
void inputRawKey(String name, int keyCode, int scanCode, bool down) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
bind.sessionHandleFlutterKeyEvent(
id: id,
name: name,
keycode: keyCode,
scancode: scanCode,
lockModes: lockModes,
downOrUp: down);
}
@@ -183,20 +207,42 @@ class InputModel {
Map<String, dynamic> getEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {};
out['type'] = type;
out['x'] = evt.position.dx;
out['y'] = evt.position.dy;
if (alt) out['alt'] = 'true';
if (shift) out['shift'] = 'true';
if (ctrl) out['ctrl'] = 'true';
if (command) out['command'] = 'true';
out['buttons'] = evt
.buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right)
if (evt.buttons != 0) {
_lastMouseDownButtons = evt.buttons;
// Check update event type and set buttons to be sent.
int buttons = _lastButtons;
if (type == _kMouseEventMove) {
// flutter may emit move event if one button is pressed and another button
// is pressing or releasing.
if (evt.buttons != _lastButtons) {
// For simplicity
// Just consider 3 - 1 ((Left + Right buttons) - Left button)
// Do not consider 2 - 1 (Right button - Left button)
// or 6 - 5 ((Right + Mid buttons) - (Left + Mid buttons))
// and so on
buttons = evt.buttons - _lastButtons;
if (buttons > 0) {
type = _kMouseEventDown;
} else {
type = _kMouseEventUp;
buttons = -buttons;
}
}
} else {
out['buttons'] = _lastMouseDownButtons;
if (evt.buttons != 0) {
buttons = evt.buttons;
}
}
_lastButtons = evt.buttons;
out['buttons'] = buttons;
out['type'] = type;
return out;
}
@@ -260,7 +306,7 @@ class InputModel {
isPhysicalMouse.value = true;
}
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, 'mousemove'));
handleMouse(getEvent(e, _kMouseEventMove));
}
}
@@ -325,21 +371,21 @@ class InputModel {
}
}
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, 'mousedown'));
handleMouse(getEvent(e, _kMouseEventDown));
}
}
void onPointUpImage(PointerUpEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, 'mouseup'));
handleMouse(getEvent(e, _kMouseEventUp));
}
}
void onPointMoveImage(PointerMoveEvent e) {
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
handleMouse(getEvent(e, 'mousemove'));
handleMouse(getEvent(e, _kMouseEventMove));
}
}
@@ -388,13 +434,13 @@ class InputModel {
var type = '';
var isMove = false;
switch (evt['type']) {
case 'mousedown':
case _kMouseEventDown:
type = 'down';
break;
case 'mouseup':
case _kMouseEventUp:
type = 'up';
break;
case 'mousemove':
case _kMouseEventMove:
isMove = true;
break;
default:
@@ -440,15 +486,21 @@ class InputModel {
evt['y'] = '${y.round()}';
var buttons = '';
switch (evt['buttons']) {
case 1:
case kPrimaryMouseButton:
buttons = 'left';
break;
case 2:
case kSecondaryMouseButton:
buttons = 'right';
break;
case 4:
case kMiddleMouseButton:
buttons = 'wheel';
break;
case kBackMouseButton:
buttons = 'back';
break;
case kForwardMouseButton:
buttons = 'forward';
break;
}
evt['buttons'] = buttons;
bind.sessionSendMouse(id: id, msg: json.encode(evt));

View File

@@ -12,6 +12,7 @@ import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -19,8 +20,9 @@ import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:tuple/tuple.dart';
import 'package:image/image.dart' as img2;
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
import 'package:flutter_custom_cursor/cursor_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import '../common.dart';
import '../common/shared_state.dart';
@@ -59,7 +61,7 @@ class FfiModel with ChangeNotifier {
bool get touchMode => _touchMode;
bool get isPeerAndroid => _pi.platform == 'Android';
bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid;
set inputBlocked(v) {
_inputBlocked = v;
@@ -140,7 +142,7 @@ class FfiModel with ChangeNotifier {
setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
} else if (name == 'switch_display') {
handleSwitchDisplay(evt);
handleSwitchDisplay(evt, peerId);
} else if (name == 'cursor_data') {
await parent.target?.cursorModel.updateCursorData(evt);
} else if (name == 'cursor_id') {
@@ -213,7 +215,7 @@ class FfiModel with ChangeNotifier {
}
}
handleSwitchDisplay(Map<String, dynamic> evt) {
handleSwitchDisplay(Map<String, dynamic> evt, String peerId) {
final oldOrientation = _display.width > _display.height;
var old = _pi.currentDisplay;
_pi.currentDisplay = int.parse(evt['display']);
@@ -221,15 +223,26 @@ class FfiModel with ChangeNotifier {
_display.y = double.parse(evt['y']);
_display.width = int.parse(evt['width']);
_display.height = int.parse(evt['height']);
_display.cursorEmbeded = int.parse(evt['cursor_embeded']) == 1;
_display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1;
if (old != _pi.currentDisplay) {
parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y);
}
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
//
}
// remote is mobile, and orientation changed
if ((_display.width > _display.height) != oldOrientation) {
gFFI.canvasModel.updateViewStyle();
}
if (_pi.platform == kPeerPlatformLinux ||
_pi.platform == kPeerPlatformWindows ||
_pi.platform == kPeerPlatformMacOS) {
parent.target?.canvasModel.updateViewStyle();
}
parent.target?.recordingModel.onSwitchDisplay();
notifyListeners();
}
@@ -331,7 +344,7 @@ class FfiModel with ChangeNotifier {
d.y = d0['y'].toDouble();
d.width = d0['width'];
d.height = d0['height'];
d.cursorEmbeded = d0['cursor_embeded'] == 1;
d.cursorEmbedded = d0['cursor_embedded'] == 1;
_pi.displays.add(d);
}
if (_pi.currentDisplay < _pi.displays.length) {
@@ -380,12 +393,22 @@ class ImageModel with ChangeNotifier {
WeakReference<FFI> parent;
final List<Function(String)> _callbacksOnFirstImage = [];
ImageModel(this.parent);
addCallbackOnFirstImage(Function(String) cb) =>
_callbacksOnFirstImage.add(cb);
onRgba(Uint8List rgba) {
if (_waitForImage[id]!) {
_waitForImage[id] = false;
parent.target?.dialogManager.dismissAll();
if (isDesktop) {
for (final cb in _callbacksOnFirstImage) {
cb(id);
}
}
}
final pid = parent.target?.id;
ui.decodeImageFromPixels(
@@ -495,7 +518,7 @@ class ViewStyle {
double get scale {
double s = 1.0;
if (style == 'adaptive') {
if (style == kRemoteViewStyleAdaptive) {
final s1 = width / displayWidth;
final s2 = height / displayHeight;
s = s1 < s2 ? s1 : s2;
@@ -511,6 +534,7 @@ class CanvasModel with ChangeNotifier {
double _y = 0;
// image scale
double _scale = 1.0;
Size _size = Size.zero;
// the tabbar over the image
// double tabBarHeight = 0.0;
// the window border's width
@@ -524,6 +548,8 @@ class CanvasModel with ChangeNotifier {
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
ViewStyle _lastViewStyle = ViewStyle();
final _imageOverflow = false.obs;
WeakReference<FFI> parent;
CanvasModel(this.parent);
@@ -531,7 +557,12 @@ class CanvasModel with ChangeNotifier {
double get x => _x;
double get y => _y;
double get scale => _scale;
Size get size => _size;
ScrollStyle get scrollStyle => _scrollStyle;
ViewStyle get viewStyle => _lastViewStyle;
RxBool get imageOverflow => _imageOverflow;
_resetScroll() => setScrollPercent(0.0, 0.0);
setScrollPercent(double x, double y) {
_scrollX = x;
@@ -542,28 +573,44 @@ class CanvasModel with ChangeNotifier {
double get scrollY => _scrollY;
updateViewStyle() async {
Size getSize() {
final size = MediaQueryData.fromWindow(ui.window).size;
// If minimized, w or h may be negative here.
double w = size.width - windowBorderWidth * 2;
double h = size.height - tabBarHeight - windowBorderWidth * 2;
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
}
final style = await bind.sessionGetViewStyle(id: id);
if (style == null) {
return;
}
final sizeWidth = size.width;
final sizeHeight = size.height;
_size = getSize();
final displayWidth = getDisplayWidth();
final displayHeight = getDisplayHeight();
final viewStyle = ViewStyle(
style: style,
width: sizeWidth,
height: sizeHeight,
width: size.width,
height: size.height,
displayWidth: displayWidth,
displayHeight: displayHeight,
);
if (_lastViewStyle == viewStyle) {
return;
}
if (_lastViewStyle.style != viewStyle.style) {
_resetScroll();
}
_lastViewStyle = viewStyle;
_scale = viewStyle.scale;
_x = (sizeWidth - displayWidth * _scale) / 2;
_y = (sizeHeight - displayHeight * _scale) / 2;
if (kIgnoreDpi && style == kRemoteViewStyleOriginal) {
_scale = 1.0 / ui.window.devicePixelRatio;
}
_x = (size.width - displayWidth * _scale) / 2;
_y = (size.height - displayHeight * _scale) / 2;
_imageOverflow.value = _x < 0 || y < 0;
notifyListeners();
}
@@ -571,8 +618,7 @@ class CanvasModel with ChangeNotifier {
final style = await bind.sessionGetScrollStyle(id: id);
if (style == kRemoteScrollStyleBar) {
_scrollStyle = ScrollStyle.scrollbar;
_scrollX = 0.0;
_scrollY = 0.0;
_resetScroll();
} else {
_scrollStyle = ScrollStyle.scrollauto;
}
@@ -586,8 +632,8 @@ class CanvasModel with ChangeNotifier {
notifyListeners();
}
bool get cursorEmbeded =>
parent.target?.ffiModel.display.cursorEmbeded ?? false;
bool get cursorEmbedded =>
parent.target?.ffiModel.display.cursorEmbedded ?? false;
int getDisplayWidth() {
final defaultWidth = (isDesktop || isWebDesktop)
@@ -606,14 +652,6 @@ class CanvasModel with ChangeNotifier {
double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
double get tabBarHeight => stateGlobal.tabBarHeight;
Size get size {
final size = MediaQueryData.fromWindow(ui.window).size;
// If minimized, w or h may be negative here.
double w = size.width - windowBorderWidth * 2;
double h = size.height - tabBarHeight - windowBorderWidth * 2;
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
}
moveDesktopMouse(double x, double y) {
// On mobile platforms, move the canvas with the cursor.
final dw = getDisplayWidth() * _scale;
@@ -1113,7 +1151,8 @@ class CursorModel with ChangeNotifier {
_clearCache() {
final keys = {...cachedKeys};
for (var k in keys) {
customCursorController.freeCache(k);
debugPrint("deleting cursor with key $k");
CursorManager.instance.deleteCursor(k);
}
}
}
@@ -1220,6 +1259,7 @@ class FFI {
late final ChatModel chatModel; // session
late final FileModel fileModel; // session
late final AbModel abModel; // global
late final GroupModel groupModel; // global
late final UserModel userModel; // global
late final QualityMonitorModel qualityMonitorModel; // session
late final RecordingModel recordingModel; // recording
@@ -1233,8 +1273,9 @@ class FFI {
serverModel = ServerModel(WeakReference(this));
chatModel = ChatModel(WeakReference(this));
fileModel = FileModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
userModel = UserModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
groupModel = GroupModel(WeakReference(this));
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
recordingModel = RecordingModel(WeakReference(this));
inputModel = InputModel(WeakReference(this));
@@ -1318,7 +1359,7 @@ class Display {
double y = 0;
int width = 0;
int height = 0;
bool cursorEmbeded = false;
bool cursorEmbedded = false;
Display() {
width = (isDesktop || isWebDesktop)

View File

@@ -9,6 +9,7 @@ import '../consts.dart';
class StateGlobal {
int _windowId = -1;
bool _fullscreen = false;
bool grabKeyboard = false;
final RxBool _showTabBar = true.obs;
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);

View File

@@ -1,7 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
@@ -10,17 +11,19 @@ import 'model.dart';
import 'platform_model.dart';
class UserModel {
var userName = ''.obs;
final RxString userName = ''.obs;
final RxString groupName = ''.obs;
final RxBool isAdmin = false.obs;
WeakReference<FFI> parent;
UserModel(this.parent) {
refreshCurrentUser();
}
UserModel(this.parent);
void refreshCurrentUser() async {
await getUserName();
final token = bind.mainGetLocalOption(key: 'access_token');
if (token == '') return;
if (token == '') {
await _updateOtherModels();
return;
}
final url = await bind.mainGetApiServer();
final body = {
'id': await bind.mainGetMyId(),
@@ -35,96 +38,95 @@ class UserModel {
body: json.encode(body));
final status = response.statusCode;
if (status == 401 || status == 400) {
resetToken();
reset();
return;
}
await _parseResp(response.body);
final data = json.decode(response.body);
final error = data['error'];
if (error != null) {
throw error;
}
final user = UserPayload.fromJson(data);
await _parseAndUpdateUser(user);
} catch (e) {
print('Failed to refreshCurrentUser: $e');
} finally {
await _updateOtherModels();
}
}
void resetToken() async {
Future<void> reset() async {
await bind.mainSetLocalOption(key: 'access_token', value: '');
await bind.mainSetLocalOption(key: 'user_info', value: '');
await gFFI.abModel.reset();
await gFFI.groupModel.reset();
userName.value = '';
groupName.value = '';
statePeerTab.check();
}
Future<String> _parseResp(String body) async {
final data = json.decode(body);
final error = data['error'];
if (error != null) {
return error!;
}
final token = data['access_token'];
if (token != null) {
await bind.mainSetLocalOption(key: 'access_token', value: token);
}
final info = data['user'];
if (info != null) {
final value = json.encode(info);
await bind.mainSetOption(key: 'user_info', value: value);
userName.value = info['name'];
}
return '';
Future<void> _parseAndUpdateUser(UserPayload user) async {
userName.value = user.name;
groupName.value = user.grp;
isAdmin.value = user.isAdmin;
}
Future<String> getUserName() async {
if (userName.isNotEmpty) {
return userName.value;
}
final userInfo = bind.mainGetLocalOption(key: 'user_info');
if (userInfo.trim().isEmpty) {
return '';
}
final m = jsonDecode(userInfo);
if (m == null) {
userName.value = '';
} else {
userName.value = m['name'] ?? '';
}
return userName.value;
Future<void> _updateOtherModels() async {
await gFFI.abModel.pullAb();
await gFFI.groupModel.pull();
}
Future<void> logOut() async {
final tag = gFFI.dialogManager.showLoading(translate('Waiting'));
final url = await bind.mainGetApiServer();
final _ = await http.post(Uri.parse('$url/api/logout'),
body: {
'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid(),
},
headers: await getHttpHeaders());
await Future.wait([
bind.mainSetLocalOption(key: 'access_token', value: ''),
bind.mainSetLocalOption(key: 'user_info', value: ''),
bind.mainSetLocalOption(key: 'selected-tags', value: ''),
]);
parent.target?.abModel.clear();
userName.value = '';
gFFI.dialogManager.dismissByTag(tag);
}
Future<Map<String, dynamic>> login(String userName, String pass) async {
final url = await bind.mainGetApiServer();
try {
final resp = await http.post(Uri.parse('$url/api/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'username': userName,
'password': pass,
'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid()
}));
final body = jsonDecode(resp.body);
bind.mainSetLocalOption(
key: 'access_token', value: body['access_token'] ?? '');
bind.mainSetLocalOption(
key: 'user_info', value: jsonEncode(body['user']));
this.userName.value = body['user']?['name'] ?? '';
return body;
} catch (err) {
return {'error': '$err'};
final url = await bind.mainGetApiServer();
await http
.post(Uri.parse('$url/api/logout'),
body: {
'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid(),
},
headers: getHttpHeaders())
.timeout(Duration(seconds: 2));
} catch (e) {
print("request /api/logout failed: err=$e");
} finally {
await reset();
gFFI.dialogManager.dismissByTag(tag);
}
}
/// throw [RequestException]
Future<LoginResponse> login(LoginRequest loginRequest) async {
final url = await bind.mainGetApiServer();
final resp = await http.post(Uri.parse('$url/api/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(loginRequest.toJson()));
final Map<String, dynamic> body;
try {
body = jsonDecode(resp.body);
} catch (e) {
print("jsonDecode resp body failed: ${e.toString()}");
rethrow;
}
if (resp.statusCode != 200) {
throw RequestException(resp.statusCode, body['error'] ?? '');
}
final LoginResponse loginResponse;
try {
loginResponse = LoginResponse.fromJson(body);
} catch (e) {
print("jsonDecode LoginResponse failed: ${e.toString()}");
rethrow;
}
if (loginResponse.user != null) {
await _parseAndUpdateUser(loginResponse.user!);
}
return loginResponse;
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
@@ -34,7 +35,7 @@ class RustDeskMultiWindowManager {
static final instance = RustDeskMultiWindowManager._();
final List<int> _activeWindows = List.empty(growable: true);
final List<VoidCallback> _windowActiveCallbacks = List.empty(growable: true);
final List<AsyncCallback> _windowActiveCallbacks = List.empty(growable: true);
int? _remoteDesktopWindowId;
int? _fileTransferWindowId;
int? _portForwardWindowId;
@@ -191,41 +192,41 @@ class RustDeskMultiWindowManager {
return _activeWindows;
}
void _notifyActiveWindow() {
Future<void> _notifyActiveWindow() async {
for (final callback in _windowActiveCallbacks) {
callback.call();
await callback.call();
}
}
void registerActiveWindow(int windowId) {
Future<void> registerActiveWindow(int windowId) async {
if (_activeWindows.contains(windowId)) {
// ignore
} else {
_activeWindows.add(windowId);
}
_notifyActiveWindow();
await _notifyActiveWindow();
}
/// Remove active window which has [`windowId`]
///
/// [Avaliability]
/// [Availability]
/// This function should only be called from main window.
/// For other windows, please post a unregister(hide) event to main window handler:
/// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});`
void unregisterActiveWindow(int windowId) {
Future<void> unregisterActiveWindow(int windowId) async {
if (!_activeWindows.contains(windowId)) {
// ignore
} else {
_activeWindows.remove(windowId);
}
_notifyActiveWindow();
await _notifyActiveWindow();
}
void registerActiveWindowListener(VoidCallback callback) {
void registerActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.add(callback);
}
void unregisterActiveWindowListener(VoidCallback callback) {
void unregisterActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.remove(callback);
}
}

View File

@@ -9,7 +9,7 @@ set(BINARY_NAME "rustdesk")
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.carriez.flutter_hbb")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# Explicitly opt into modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)

View File

@@ -70,7 +70,7 @@ target_link_libraries(flutter INTERFACE
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# _phony_ is a nonexistent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(

View File

@@ -23,7 +23,15 @@ static void my_application_activate(GApplication* application) {
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// we have custom window frame
gtk_window_set_decorated(window, FALSE);
// try setting icon for rustdesk, which uses the system cache
GtkIconTheme* theme = gtk_icon_theme_get_default();
gint icons[4] = {256, 128, 64, 32};
for (int i = 0; i < 4; i++) {
GdkPixbuf* icon = gtk_icon_theme_load_icon(theme, "rustdesk", icons[i], GTK_ICON_LOOKUP_NO_SVG, NULL);
if (icon != nullptr) {
gtk_window_set_icon(window, icon);
}
}
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).

View File

@@ -26,6 +26,10 @@
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */; };
7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */; };
7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */; };
7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */; };
84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (Weak, ); }; };
84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; };
@@ -74,6 +78,10 @@
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light.png"; path = "../../res/mac-tray-light.png"; sourceTree = "<group>"; };
7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark.png"; path = "../../res/mac-tray-dark.png"; sourceTree = "<group>"; };
7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light-x2.png"; path = "../../res/mac-tray-light-x2.png"; sourceTree = "<group>"; };
7E881463296E991200A0C54F /* mac-tray-dark-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark-x2.png"; path = "../../res/mac-tray-dark-x2.png"; sourceTree = "<group>"; };
84010BA7292CF66600152837 /* liblibrustdesk.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = liblibrustdesk.dylib; path = ../../target/release/liblibrustdesk.dylib; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
@@ -127,6 +135,10 @@
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
7E881463296E991200A0C54F /* mac-tray-dark-x2.png */,
7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */,
7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */,
7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */,
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
@@ -253,8 +265,12 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */,
7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */,
7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */,
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -378,7 +394,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = x86_64;
ARCHS = "$(ARCHS_STANDARD)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -403,6 +419,7 @@
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -428,8 +445,11 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -459,7 +479,7 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = x86_64;
ARCHS = "$(ARCHS_STANDARD)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -513,7 +533,7 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = x86_64;
ARCHS = "$(ARCHS_STANDARD)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -538,6 +558,7 @@
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -550,6 +571,12 @@
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = (
"-sectcreate",
__CGPreLoginApp,
__cgpreloginapp,
/dev/null,
);
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
@@ -563,8 +590,10 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -590,8 +619,11 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -602,6 +634,12 @@
../../target/release,
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
OTHER_LDFLAGS = (
"-sectcreate",
__CGPreLoginApp,
__cgpreloginapp,
/dev/null,
);
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk;
PROVISIONING_PROFILE_SPECIFIER = "";
"SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 B

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 909 B

After

Width:  |  Height:  |  Size: 901 B

View File

@@ -6,6 +6,8 @@
<false/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>

View File

@@ -49,7 +49,8 @@ class MainFlutterWindow: NSWindow {
super.awakeFromNib()
}
// override func bitsdojo_window_configure() -> UInt {
// return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP
// }
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
super.order(place, relativeTo: otherWin)
hiddenWindowAtLaunch()
}
}

View File

@@ -4,6 +4,10 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>

View File

@@ -108,6 +108,12 @@
PRODUCT_NAME = rustdesk;
SDKROOT = macosx;
SUPPORTS_MACCATALYST = YES;
OTHER_LDFLAGS = (
"-sectcreate",
__CGPreLoginApp,
__cgpreloginapp,
/dev/null,
);
};
name = Release;
};

View File

@@ -7,7 +7,7 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "49.0.0"
version: "50.0.0"
after_layout:
dependency: transitive
description:
@@ -21,7 +21,7 @@ packages:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.0"
version: "5.2.0"
animations:
dependency: transitive
description:
@@ -35,7 +35,7 @@ packages:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.1"
version: "3.3.5"
args:
dependency: transitive
description:
@@ -63,7 +63,7 @@ packages:
name: back_button_interceptor
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.1"
version: "6.0.2"
bot_toast:
dependency: "direct main"
description:
@@ -84,7 +84,7 @@ packages:
name: build_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.1"
build_daemon:
dependency: transitive
description:
@@ -105,14 +105,14 @@ packages:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
version: "2.3.3"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "7.2.4"
version: "7.2.7"
built_collection:
dependency: transitive
description:
@@ -126,7 +126,7 @@ packages:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "8.4.1"
version: "8.4.2"
cached_network_image:
dependency: transitive
description:
@@ -154,7 +154,7 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
version: "1.2.0"
charcode:
dependency: transitive
description:
@@ -175,14 +175,14 @@ packages:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
version: "1.1.0"
code_builder:
dependency: transitive
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.0"
version: "4.4.0"
collection:
dependency: transitive
description:
@@ -203,7 +203,7 @@ packages:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
version: "3.1.0"
cross_file:
dependency: transitive
description:
@@ -264,8 +264,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75"
resolved-ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75"
ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124"
resolved-ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124"
url: "https://github.com/Kingtous/rustdesk_desktop_multi_window"
source: git
version: "0.1.0"
@@ -352,7 +352,7 @@ packages:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.1"
version: "5.2.4"
fixnum:
dependency: transitive
description:
@@ -389,12 +389,10 @@ packages:
flutter_custom_cursor:
dependency: "direct main"
description:
path: "."
ref: "74b1b314142b6775c1243067a3503ac568ebc74b"
resolved-ref: "74b1b314142b6775c1243067a3503ac568ebc74b"
url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor"
source: git
version: "0.0.1"
name: flutter_custom_cursor
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2"
flutter_improved_scrolling:
dependency: "direct main"
description:
@@ -443,7 +441,7 @@ packages:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.5"
version: "1.1.6"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -455,21 +453,21 @@ packages:
name: freezed
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
version: "2.3.2"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.2.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
version: "3.2.0"
get:
dependency: "direct main"
description:
@@ -483,21 +481,21 @@ packages:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.1"
graphs:
dependency: transitive
description:
name: graphs
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.2.0"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.0"
version: "0.15.1"
http:
dependency: "direct main"
description:
@@ -518,7 +516,7 @@ packages:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
version: "4.0.2"
icons_launcher:
dependency: "direct dev"
description:
@@ -532,7 +530,7 @@ packages:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
version: "3.2.2"
image_picker:
dependency: "direct main"
description:
@@ -546,7 +544,7 @@ packages:
name: image_picker_android
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.5+3"
version: "0.8.5+4"
image_picker_for_web:
dependency: transitive
description:
@@ -560,7 +558,7 @@ packages:
name: image_picker_ios
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.6+1"
version: "0.8.6+3"
image_picker_platform_interface:
dependency: transitive
description:
@@ -602,7 +600,7 @@ packages:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.0.1"
logging:
dependency: transitive
description:
@@ -623,21 +621,21 @@ packages:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.5"
version: "0.1.4"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
version: "1.7.0"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
version: "1.0.3"
nested:
dependency: transitive
description:
@@ -707,7 +705,7 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
version: "1.8.1"
path_drawing:
dependency: transitive
description:
@@ -735,7 +733,7 @@ packages:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.20"
version: "2.0.22"
path_provider_ios:
dependency: transitive
description:
@@ -799,6 +797,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
pointycastle:
dependency: transitive
description:
name: pointycastle
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.2"
pool:
dependency: transitive
description:
@@ -819,14 +824,14 @@ packages:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.3"
version: "6.0.5"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
version: "2.1.3"
pubspec_parse:
dependency: transitive
description:
@@ -847,7 +852,7 @@ packages:
name: rxdart
url: "https://pub.dartlang.org"
source: hosted
version: "0.27.5"
version: "0.27.7"
screen_retriever:
dependency: transitive
description:
@@ -884,7 +889,7 @@ packages:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
version: "1.0.3"
simple_observable:
dependency: transitive
description:
@@ -903,7 +908,7 @@ packages:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.5"
version: "1.2.6"
source_span:
dependency: transitive
description:
@@ -924,7 +929,7 @@ packages:
name: sqflite_common
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.0"
version: "2.4.0"
stack_trace:
dependency: transitive
description:
@@ -945,7 +950,7 @@ packages:
name: stream_transform
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
version: "2.1.0"
string_scanner:
dependency: transitive
description:
@@ -1036,14 +1041,14 @@ packages:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.6"
version: "6.1.7"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.19"
version: "6.0.22"
url_launcher_ios:
dependency: transitive
description:
@@ -1092,7 +1097,7 @@ packages:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.6"
version: "3.0.7"
vector_math:
dependency: transitive
description:
@@ -1106,35 +1111,35 @@ packages:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.7"
version: "2.4.10"
video_player_android:
dependency: transitive
description:
name: video_player_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.9"
version: "2.3.10"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.7"
version: "2.3.8"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.4"
version: "6.0.1"
video_player_web:
dependency: transitive
description:
name: video_player_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.12"
version: "2.0.13"
visibility_detector:
dependency: "direct main"
description:
@@ -1183,7 +1188,7 @@ packages:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.0.2"
web_socket_channel:
dependency: transitive
description:
@@ -1197,7 +1202,7 @@ packages:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "3.1.3"
win32_registry:
dependency: transitive
description:

View File

@@ -63,12 +63,9 @@ dependencies:
desktop_multi_window:
git:
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
ref: 82f9eab81cb2c7bfb938def7a1b399a6279bbc75
ref: 057e6eb1bc7dcbcf9dafd1384274a611e4fe7124
freezed_annotation: ^2.0.3
flutter_custom_cursor:
git:
url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor
ref: 74b1b314142b6775c1243067a3503ac568ebc74b
flutter_custom_cursor: ^0.0.2
window_size:
git:
url: https://github.com/google/flutter-desktop-embedding.git

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
import re
import re
import os
import glob
from tabnanny import check
@@ -69,7 +69,7 @@ def main():
for ln in open('../../../Cargo.toml', encoding='utf-8'):
if ln.startswith('version ='):
print('export const ' + ln)
def removeComment(ln):
return re.sub('\s+\/\/.*$', '', ln)

View File

@@ -22,8 +22,8 @@
import { simd } from "wasm-feature-detect";
export async function loadVp9(callback) {
// Multithreading is used only if `options.threading` is true.
// This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs,
// Multithreading is used only if `options.threading` is true.
// This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs,
// currently available in Firefox and Chrome with experimental flags enabled.
// 所有主流浏览器均默认于2018年1月5日禁用SharedArrayBuffer
const isSIMD = await simd();

View File

@@ -82,10 +82,10 @@ export default class Connection {
this._ws = ws;
this._id = id;
console.log(
new Date() + ": Conntecting to rendezvoous server: " + uri + ", for " + id
new Date() + ": Connecting to rendezvous server: " + uri + ", for " + id
);
await ws.open();
console.log(new Date() + ": Connected to rendezvoous server");
console.log(new Date() + ": Connected to rendezvous server");
const conn_type = rendezvous.ConnType.DEFAULT_CONN;
const nat_type = rendezvous.NatType.SYMMETRIC;
const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({

View File

@@ -6,7 +6,7 @@ project(rustdesk LANGUAGES CXX)
# the on-disk name of your application.
set(BINARY_NAME "rustdesk")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# Explicitly opt into modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)

View File

@@ -79,7 +79,7 @@ target_include_directories(flutter_wrapper_app PUBLIC
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# _phony_ is a nonexistent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")

View File

@@ -43,7 +43,7 @@ class WindowClassRegistrar {
public:
~WindowClassRegistrar() = default;
// Returns the singleton registar instance.
// Returns the singleton registrar instance.
static WindowClassRegistrar* GetInstance() {
if (!instance_) {
instance_ = new WindowClassRegistrar();
@@ -116,7 +116,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title,
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),

View File

@@ -31,7 +31,7 @@ class Win32Window {
// Creates and shows a win32 window with |title| and position and size using
// |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size to will treat the width height passed in to this function
// consistent size to will treat the width height passed into this function
// as logical pixels and scale to appropriate for the default monitor. Returns
// true if the window was created successfully.
bool CreateAndShow(const std::wstring& title,
@@ -77,7 +77,7 @@ class Win32Window {
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
// non-client DPI scaling so that the non-client area automatically
// responsponds to changes in DPI. All other messages are handled by
// responds to changes in DPI. All other messages are handled by
// MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window,
UINT const message,