rustdesk/flutter/lib/common.dart

1563 lines
51 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:convert';
import 'dart:ffi' hide Size;
import 'dart:io';
2022-11-09 15:14:11 +08:00
import 'dart:math';
import 'dart:typed_data';
import 'package:back_button_interceptor/back_button_interceptor.dart';
2022-08-09 19:32:19 +08:00
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:win32/win32.dart' as win32;
import 'package:flutter/gestures.dart';
2020-11-15 20:04:05 +08:00
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
2022-10-11 19:52:03 +08:00
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
2022-10-18 10:29:33 +08:00
import 'package:uni_links/uni_links.dart';
import 'package:uni_links_desktop/uni_links_desktop.dart';
import 'package:window_manager/window_manager.dart';
2022-09-27 18:34:05 +08:00
import 'package:flutter_svg/flutter_svg.dart';
import 'package:window_size/window_size.dart' as window_size;
import 'package:url_launcher/url_launcher.dart';
2022-04-19 13:07:45 +08:00
2022-09-13 09:03:34 +08:00
import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
2022-09-27 20:35:02 +08:00
import 'models/input_model.dart';
2022-04-19 13:07:45 +08:00
import 'models/model.dart';
import 'models/platform_model.dart';
2021-08-02 20:54:56 +08:00
import '../consts.dart';
2022-03-07 22:54:34 +08:00
final globalKey = GlobalKey<NavigatorState>();
2022-04-12 22:38:39 +08:00
final navigationBarKey = GlobalKey();
2022-03-07 22:54:34 +08:00
2022-08-23 15:25:18 +08:00
final isAndroid = Platform.isAndroid;
final isIOS = Platform.isIOS;
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
var isWeb = false;
2022-05-23 16:02:37 +08:00
var isWebDesktop = false;
var version = "";
2022-03-24 17:58:33 +08:00
int androidVersion = 0;
/// only avaliable for Windows target
int windowsBuildNumber = 0;
2022-09-15 16:36:52 +08:00
DesktopType? desktopType;
2022-10-20 22:05:34 +08:00
/// * debug or test only, DO NOT enable in release build
bool isTest = false;
2021-08-02 20:54:56 +08:00
typedef F = String Function(String);
2022-02-10 02:07:53 +08:00
typedef FMethod = String Function(String, dynamic);
2021-08-02 20:54:56 +08:00
typedef StreamEventHandler = Future<void> Function(Map<String, dynamic>);
final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode(
"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII=")));
final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode(
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII=')));
final iconAudio = MemoryImage(Uint8List.fromList(base64Decode(
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg==')));
final iconFile = MemoryImage(Uint8List.fromList(base64Decode(
'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg==')));
final iconRestart = MemoryImage(Uint8List.fromList(base64Decode(
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC')));
final iconRecording = MemoryImage(Uint8List.fromList(base64Decode(
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC')));
final iconHardDrive = MemoryImage(Uint8List.fromList(base64Decode(
'iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAmVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjHWqVAAAAMnRSTlMAv0BmzLJNXlhiUu2fxXDgu7WuSUUe29LJvpqUjX53VTstD7ilNujCqTEk5IYH+vEoFjKvAagAAAPpSURBVHja7d0JbhpBEIXhB3jYzb5vBgzYgO04df/DJXGUKMwU9ECmZ6pQfSfw028LCXW3YYwxxhhjjDHGGGOM0eZ9VV1MckdKWLM1bRQ/35GW/WxHHu1me6ShuyHvNl34VhlTKsYVeDWj1EzgUZ1S1DrAk/UDparZgxd9Sl0BHnxSBhpI3jfKQG2FpLUpE69I2ILikv1nsvygjBwPSNKYMlNHggqUoSKS80AZCnwHqQ1zCRvW+CRegwRFeFAMKKrtM8gTPJlzSfwFgT9dJom3IDN4VGaSeAryAK8m0SSeghTg1ZYiql6CjBDhO8mzlyAVhKhIwgXxrh5NojGIhyRckEdwpCdhgpSQgiWTRGMQNonGIGySp0SDvMDBX5KWxiB8Eo1BgE00SYJBykhNnkmSWJAcLpGaJNMgfJKyxiDAK4WNEwryhMtkJsk8CJtEYxA+icYgQIfCcgkEqcJNXhIRQdgkGoPwSTQG+e8khdu/7JOVREwQIKCwF41B2CQljUH4JLcH6SI+OUlEBQHa0SQag/BJNAbhkjxqDMIn0RgEeI4muSlID9eSkERgEKAVTaIxCJ9EYxA2ydVB8hCASVLRGAQYR5NoDMIn0RgEyFHYSGMQPonGII4kziCNvBgNJonEk4u3GAk8Sprk6eYaqbMDY0oKvUm5jfC/viGiSypV7+M3i2iDsAGpNEDYjlTa3W8RdR/r544g50ilnA0RxoZIE2NIXqQbhkAkGyKNDZHGhkhjQ6SxIdLYEGlsiDQ2JGTVeD0264U9zipPh7XOooffpA6pfNCXjxl4/c3pUzlChwzor53zwYYVfpI5pOV6LWFF/2jiJ5FDSs5jdY/0rwUAkUMeXWdBqnSqD0DikBqdqCHsjTvELm9In0IOri/0pwAEDtlSyNaRjAIAAoesKWTtuusxByBwCJp0oomwBXcYUuCQgE50ENajE4OvZAKHLB1/68Br5NqiyCGYOY8YRd77kTkEb64n7lZN+mOIX4QOwb5FX0ZVx3uOxwW+SB0CbBubemWP8/rlaaeRX+M3uUOuZENsiA25zIbYkPsZElBIHwL13U/PTjJ/cyOOEoVM3I+hziDQlELm7pPxw3eI8/7gPh1fpLA6xGnEeDDgO0UcIAzzM35HxLPIq5SXe9BLzOsj9eUaQqyXzxS1QFSfWM2cCANiHcAISJ0AnCKpUwTuIkkA3EeSInAXSQKcs1V18e24wlllUmQp9v9zXKeHi+akRAMOPVKhAqdPBZeUmnnEsO6QcJ0+4qmOSbBxFfGVRiTUqITrdKcCbyYO3/K4wX4+aQ+FfNjXhu3JfAVjjDHGGGOMMcYYY4xIPwCgfqT6TbhCLAAAAABJRU5ErkJggg==')));
enum DesktopType {
main,
remote,
fileTransfer,
cm,
portForward,
}
class IconFont {
static const _family1 = 'Tabbar';
static const _family2 = 'PeerSearchbar';
IconFont._();
static const IconData max = IconData(0xe606, fontFamily: _family1);
static const IconData restore = IconData(0xe607, fontFamily: _family1);
static const IconData close = IconData(0xe668, fontFamily: _family1);
static const IconData min = IconData(0xe609, fontFamily: _family1);
static const IconData add = IconData(0xe664, fontFamily: _family1);
static const IconData menu = IconData(0xe628, fontFamily: _family1);
static const IconData search = IconData(0xe6a4, fontFamily: _family2);
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
}
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, Color? highlight}) {
return ColorThemeExtension(
border: border ?? this.border,
highlight: highlight ?? this.highlight,
);
}
@override
ThemeExtension<ColorThemeExtension> lerp(
ThemeExtension<ColorThemeExtension>? other, double t) {
if (other is! ColorThemeExtension) {
return this;
}
return ColorThemeExtension(
border: Color.lerp(border, other.border, t),
highlight: Color.lerp(highlight, other.highlight, t),
);
}
}
2020-11-16 01:13:26 +08:00
class MyTheme {
2020-11-20 16:37:48 +08:00
MyTheme._();
2022-02-02 17:25:56 +08:00
2020-11-16 01:13:26 +08:00
static const Color grayBg = Color(0xFFEEEEEE);
static const Color white = Color(0xFFFFFFFF);
2020-11-16 22:00:09 +08:00
static const Color accent = Color(0xFF0071FF);
2020-11-19 17:22:42 +08:00
static const Color accent50 = Color(0x770071FF);
2020-11-27 02:14:27 +08:00
static const Color accent80 = Color(0xAA0071FF);
2020-11-19 18:41:37 +08:00
static const Color canvasColor = Color(0xFF212121);
2020-11-20 13:06:52 +08:00
static const Color border = Color(0xFFCCCCCC);
2022-01-31 16:22:05 +08:00
static const Color idColor = Color(0xFF00B6F0);
static const Color darkGray = Color(0xFFB9BABC);
static const Color cmIdColor = Color(0xFF21790B);
static const Color dark = Colors.black87;
static const Color button = Color(0xFF2C8CFF);
static const Color hoverBorder = Color(0xFF999999);
static ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
2022-09-23 16:31:50 +08:00
backgroundColor: Color(0xFFFFFFFF),
scaffoldBackgroundColor: Color(0xFFEEEEEE),
textTheme: const TextTheme(
2022-09-29 13:07:20 +08:00
titleLarge: TextStyle(fontSize: 19, color: Colors.black87),
titleSmall: TextStyle(fontSize: 14, color: Colors.black87),
bodySmall: TextStyle(fontSize: 12, color: Colors.black87, height: 1.25),
bodyMedium:
TextStyle(fontSize: 14, color: Colors.black87, height: 1.25),
labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)),
2022-09-23 16:31:50 +08:00
hintColor: Color(0xFFAAAAAA),
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
tabBarTheme: const TabBarTheme(
labelColor: Colors.black87,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
splashFactory: isDesktop ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop
? TextButtonThemeData(
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
)
: null,
).copyWith(
extensions: <ThemeExtension<dynamic>>[
ColorThemeExtension.light,
TabbarTheme.light,
],
);
static ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
2022-09-23 16:31:50 +08:00
backgroundColor: Color(0xFF252525),
scaffoldBackgroundColor: Color(0xFF141414),
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 19),
titleSmall: TextStyle(fontSize: 14),
bodySmall: TextStyle(fontSize: 12, height: 1.25),
2022-09-29 13:07:20 +08:00
bodyMedium: TextStyle(fontSize: 14, height: 1.25),
labelLarge: TextStyle(
fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)),
2022-09-23 17:16:25 +08:00
cardColor: Color(0xFF252525),
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
tabBarTheme: const TabBarTheme(
labelColor: Colors.white70,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
splashFactory: isDesktop ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop
? TextButtonThemeData(
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
)
: null,
).copyWith(
extensions: <ThemeExtension<dynamic>>[
ColorThemeExtension.dark,
TabbarTheme.dark,
],
);
static ThemeMode getThemeModePreference() {
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
}
static void changeDarkMode(ThemeMode mode) {
Get.changeThemeMode(mode);
if (desktopType == DesktopType.main) {
if (mode == ThemeMode.system) {
bind.mainSetLocalOption(key: kCommConfKeyTheme, value: '');
} else {
bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString());
}
bind.mainChangeTheme(dark: mode.toShortString());
}
}
static ThemeMode currentThemeMode() {
final preference = getThemeModePreference();
if (preference == ThemeMode.system) {
if (WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.light) {
return ThemeMode.light;
} else {
return ThemeMode.dark;
}
} else {
return preference;
}
}
static ColorThemeExtension color(BuildContext context) {
return Theme.of(context).extension<ColorThemeExtension>()!;
}
static TabbarTheme tabbar(BuildContext context) {
return Theme.of(context).extension<TabbarTheme>()!;
}
static ThemeMode themeModeFromString(String v) {
switch (v) {
case "light":
return ThemeMode.light;
case "dark":
return ThemeMode.dark;
default:
return ThemeMode.system;
}
}
}
extension ParseToString on ThemeMode {
String toShortString() {
return toString().split('.').last;
}
2020-11-16 01:13:26 +08:00
}
2021-08-02 20:54:56 +08:00
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
2022-05-18 15:47:07 +08:00
minimumSize: Size(0, 36),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
2021-08-02 20:54:56 +08:00
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2.0)),
),
);
2022-09-19 15:46:09 +08:00
List<Locale> supportedLocales = const [
// specify CN/TW to fix CJK issue in flutter
Locale('zh', 'CN'),
Locale('zh', 'TW'),
Locale('zh', 'SG'),
2022-09-19 15:46:09 +08:00
Locale('fr'),
Locale('de'),
Locale('it'),
Locale('ja'),
Locale('cs'),
Locale('pl'),
Locale('ko'),
Locale('hu'),
Locale('pt'),
Locale('ru'),
Locale('sk'),
Locale('id'),
Locale('da'),
Locale('eo'),
Locale('tr'),
Locale('vi'),
Locale('pl'),
Locale('kz'),
Locale('en', 'US'),
];
String formatDurationToTime(Duration duration) {
var totalTime = duration.inSeconds;
final secs = totalTime % 60;
totalTime = (totalTime - secs) ~/ 60;
final mins = totalTime % 60;
totalTime = (totalTime - mins) ~/ 60;
return "${totalTime.toString().padLeft(2, "0")}:${mins.toString().padLeft(2, "0")}:${secs.toString().padLeft(2, "0")}";
}
closeConnection({String? id}) {
2022-08-12 18:42:02 +08:00
if (isAndroid || isIOS) {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
} else {
final controller = Get.find<DesktopTabController>();
controller.closeBy(id);
2022-08-12 18:42:02 +08:00
}
2022-03-13 23:07:52 +08:00
}
2022-08-09 19:32:19 +08:00
void window_on_top(int? id) {
if (id == null) {
// main window
windowManager.restore();
windowManager.show();
windowManager.focus();
rustDeskWinManager.registerActiveWindow(kWindowMainId);
2022-08-09 19:32:19 +08:00
} else {
2022-08-09 21:12:55 +08:00
WindowController.fromWindowId(id)
..focus()
..show();
2022-11-05 23:41:22 +08:00
rustDeskWinManager.call(WindowType.Main, kWindowEventShow, {"id": id});
2022-08-09 19:32:19 +08:00
}
}
2022-03-13 00:32:44 +08:00
typedef DialogBuilder = CustomAlertDialog Function(
2022-04-19 13:07:45 +08:00
StateSetter setState, void Function([dynamic]) close);
2022-03-13 00:32:44 +08:00
2022-08-12 18:42:02 +08:00
class Dialog<T> {
OverlayEntry? entry;
Completer<T?> completer = Completer<T?>();
2022-08-12 18:42:02 +08:00
Dialog();
void complete(T? res) {
try {
if (!completer.isCompleted) {
completer.complete(res);
}
} catch (e) {
debugPrint("Dialog complete catch error: $e");
} finally {
entry?.remove();
2022-08-12 18:42:02 +08:00
}
}
}
class OverlayDialogManager {
OverlayState? _overlayState;
final Map<String, Dialog> _dialogs = {};
2022-08-12 18:42:02 +08:00
int _tagCount = 0;
2022-03-13 00:32:44 +08:00
OverlayEntry? _mobileActionsOverlayEntry;
2022-08-12 18:42:02 +08:00
/// By default OverlayDialogManager use global overlay
OverlayDialogManager() {
_overlayState = globalKey.currentState?.overlay;
}
2022-03-13 00:32:44 +08:00
2022-08-12 18:42:02 +08:00
void setOverlayState(OverlayState? overlayState) {
_overlayState = overlayState;
}
void dismissAll() {
_dialogs.forEach((key, value) {
value.complete(null);
BackButtonInterceptor.removeByName(key);
});
_dialogs.clear();
}
void dismissByTag(String tag) {
_dialogs[tag]?.complete(null);
_dialogs.remove(tag);
BackButtonInterceptor.removeByName(tag);
}
Future<T?> show<T>(DialogBuilder builder,
2022-04-20 23:43:19 +08:00
{bool clickMaskDismiss = false,
bool backDismiss = false,
String? tag,
2022-08-12 18:42:02 +08:00
bool useAnimation = true,
bool forceGlobal = false}) {
final overlayState =
forceGlobal ? globalKey.currentState?.overlay : _overlayState;
if (overlayState == null) {
return Future.error(
"[OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first");
}
2022-10-09 18:57:38 +08:00
final String dialogTag;
2022-04-19 13:07:45 +08:00
if (tag != null) {
2022-10-09 18:57:38 +08:00
dialogTag = tag;
2022-04-19 13:07:45 +08:00
} else {
2022-10-09 18:57:38 +08:00
dialogTag = _tagCount.toString();
2022-08-12 18:42:02 +08:00
_tagCount++;
2022-04-19 13:07:45 +08:00
}
2022-08-12 18:42:02 +08:00
final dialog = Dialog<T>();
2022-10-09 18:57:38 +08:00
_dialogs[dialogTag] = dialog;
2022-08-12 18:42:02 +08:00
2022-10-09 18:57:38 +08:00
close([res]) {
_dialogs.remove(dialogTag);
2022-08-12 18:42:02 +08:00
dialog.complete(res);
2022-10-09 18:57:38 +08:00
BackButtonInterceptor.removeByName(dialogTag);
}
2022-08-12 18:42:02 +08:00
dialog.entry = OverlayEntry(builder: (_) {
bool innerClicked = false;
return Listener(
onPointerUp: (_) {
if (!innerClicked && clickMaskDismiss) {
close();
}
innerClicked = false;
},
child: Container(
color: Colors.black12,
child: StatefulBuilder(builder: (context, setState) {
return Listener(
onPointerUp: (_) => innerClicked = true,
child: builder(setState, close),
);
})));
2022-08-12 18:42:02 +08:00
});
overlayState.insert(dialog.entry!);
BackButtonInterceptor.add((stopDefaultButtonEvent, routeInfo) {
if (backDismiss) {
close();
}
return true;
2022-10-09 18:57:38 +08:00
}, name: dialogTag);
2022-08-12 18:42:02 +08:00
return dialog.completer.future;
}
2022-10-09 18:57:38 +08:00
String showLoading(String text,
{bool clickMaskDismiss = false,
bool showCancel = true,
VoidCallback? onCancel}) {
2022-10-09 18:57:38 +08:00
final tag = _tagCount.toString();
_tagCount++;
show((setState, close) {
cancel() {
dismissAll();
if (onCancel != null) {
onCancel();
}
}
return CustomAlertDialog(
2022-08-12 18:42:02 +08:00
content: Container(
constraints: const BoxConstraints(maxWidth: 240),
2022-08-12 18:42:02 +08:00
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 30),
const Center(child: CircularProgressIndicator()),
const SizedBox(height: 20),
2022-08-12 18:42:02 +08:00
Center(
child: Text(translate(text),
style: const TextStyle(fontSize: 15))),
const SizedBox(height: 20),
Offstage(
offstage: !showCancel,
child: Center(
child: TextButton(
style: flatButtonStyle,
onPressed: cancel,
child: Text(translate('Cancel'),
style:
const TextStyle(color: MyTheme.accent)))))
])),
onCancel: showCancel ? cancel : null,
);
2022-10-09 18:57:38 +08:00
}, tag: tag);
return tag;
2022-08-12 18:42:02 +08:00
}
void resetMobileActionsOverlay({FFI? ffi}) {
if (_mobileActionsOverlayEntry == null) return;
hideMobileActionsOverlay();
showMobileActionsOverlay(ffi: ffi);
}
void showMobileActionsOverlay({FFI? ffi}) {
if (_mobileActionsOverlayEntry != null) return;
if (_overlayState == null) return;
// compute overlay position
final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
final screenH = MediaQuery.of(globalKey.currentContext!).size.height;
const double overlayW = 200;
const double overlayH = 45;
final left = (screenW - overlayW) / 2;
final top = screenH - overlayH - 80;
final overlay = OverlayEntry(builder: (context) {
final session = ffi ?? gFFI;
return DraggableMobileActions(
position: Offset(left, top),
width: overlayW,
height: overlayH,
2022-09-27 20:35:02 +08:00
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
onRecentPressed: () async {
2022-09-27 20:35:02 +08:00
session.inputModel.sendMouse('down', MouseButtons.wheel);
await Future.delayed(const Duration(milliseconds: 500));
2022-09-27 20:35:02 +08:00
session.inputModel.sendMouse('up', MouseButtons.wheel);
},
onHidePressed: () => hideMobileActionsOverlay(),
);
});
_overlayState!.insert(overlay);
_mobileActionsOverlayEntry = overlay;
}
void hideMobileActionsOverlay() {
if (_mobileActionsOverlayEntry != null) {
_mobileActionsOverlayEntry!.remove();
_mobileActionsOverlayEntry = null;
return;
}
}
void toggleMobileActionsOverlay({FFI? ffi}) {
if (_mobileActionsOverlayEntry == null) {
showMobileActionsOverlay(ffi: ffi);
} else {
hideMobileActionsOverlay();
}
}
}
2022-08-12 18:42:02 +08:00
void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
final overlayState = globalKey.currentState?.overlay;
if (overlayState == null) return;
final entry = OverlayEntry(builder: (_) {
return IgnorePointer(
child: Align(
alignment: const Alignment(0.0, 0.8),
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
child: Text(
text,
style: const TextStyle(
decoration: TextDecoration.none,
fontWeight: FontWeight.w300,
fontSize: 18,
color: Colors.white),
),
)));
});
overlayState.insert(entry);
Future.delayed(timeout, () {
entry.remove();
});
2022-02-28 16:11:21 +08:00
}
2022-02-02 17:25:56 +08:00
2022-03-13 00:32:44 +08:00
class CustomAlertDialog extends StatelessWidget {
const CustomAlertDialog(
{Key? key,
this.title,
required this.content,
this.actions,
this.contentPadding,
this.contentBoxConstraints = const BoxConstraints(maxWidth: 500),
this.onSubmit,
this.onCancel})
: super(key: key);
2020-11-18 12:49:43 +08:00
2022-08-12 18:42:02 +08:00
final Widget? title;
2022-03-13 00:32:44 +08:00
final Widget content;
2022-08-12 18:42:02 +08:00
final List<Widget>? actions;
2022-03-13 00:32:44 +08:00
final double? contentPadding;
final BoxConstraints contentBoxConstraints;
final Function()? onSubmit;
final Function()? onCancel;
2022-03-13 00:32:44 +08:00
@override
Widget build(BuildContext context) {
FocusNode focusNode = FocusNode();
// request focus if there is no focused FocusNode in the dialog
Future.delayed(Duration.zero, () {
if (!focusNode.hasFocus) focusNode.requestFocus();
});
return Focus(
focusNode: focusNode,
autofocus: true,
onKey: (node, key) {
if (key.logicalKey == LogicalKeyboardKey.escape) {
if (key is RawKeyDownEvent) {
onCancel?.call();
}
return KeyEventResult.handled; // avoid TextField exception on escape
} else if (onSubmit != null &&
key.logicalKey == LogicalKeyboardKey.enter) {
if (key is RawKeyDownEvent) onSubmit?.call();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: AlertDialog(
scrollable: true,
title: title,
contentPadding: EdgeInsets.symmetric(
horizontal: contentPadding ?? 25, vertical: 10),
content:
ConstrainedBox(constraints: contentBoxConstraints, child: content),
actions: actions,
),
2022-04-21 10:02:47 +08:00
);
2022-03-13 00:32:44 +08:00
}
2020-11-16 22:00:09 +08:00
}
2020-11-16 22:12:32 +08:00
void msgBox(String id, String type, String title, String text, String link,
OverlayDialogManager dialogManager,
2022-08-12 18:42:02 +08:00
{bool? hasCancel}) {
dialogManager.dismissAll();
List<Widget> buttons = [];
bool hasOk = false;
submit() {
dialogManager.dismissAll();
// https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
if (!type.contains("custom") && desktopType != DesktopType.portForward) {
closeConnection();
}
}
cancel() {
dialogManager.dismissAll();
}
jumplink() {
if (link.startsWith('http')) {
launchUrl(Uri.parse(link));
}
}
if (type != "connecting" && type != "success" && !type.contains("nook")) {
hasOk = true;
buttons.insert(0, msgBoxButton(translate('OK'), submit));
}
hasCancel ??= !type.contains("error") &&
!type.contains("nocancel") &&
type != "restarting";
2020-11-29 01:36:10 +08:00
if (hasCancel) {
buttons.insert(0, msgBoxButton(translate('Cancel'), cancel));
2020-11-29 01:36:10 +08:00
}
// TODO: test this button
if (type.contains("hasclose")) {
buttons.insert(
0,
msgBoxButton(translate('Close'), () {
2022-08-12 18:42:02 +08:00
dialogManager.dismissAll();
}));
}
if (link.isNotEmpty) {
buttons.insert(0, msgBoxButton(translate('JumpLink'), jumplink));
}
dialogManager.show(
(setState, close) => CustomAlertDialog(
title: _msgBoxTitle(title),
content:
SelectableText(translate(text), style: const TextStyle(fontSize: 15)),
actions: buttons,
onSubmit: hasOk ? submit : null,
onCancel: hasCancel == true ? cancel : null,
),
tag: '$id-$type-$title-$text-$link',
);
2020-11-21 14:40:28 +08:00
}
2020-11-25 18:33:09 +08:00
Widget msgBoxButton(String text, void Function() onPressed) {
return ButtonTheme(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
//limits the touch area to the button area
minWidth: 0,
//wraps child's width
height: 0,
child: TextButton(
style: flatButtonStyle,
onPressed: onPressed,
child:
Text(translate(text), style: TextStyle(color: MyTheme.accent))));
}
Widget _msgBoxTitle(String title) =>
Text(translate(title), style: TextStyle(fontSize: 21));
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
Widget content, List<Widget> buttons,
{bool hasCancel = true}) {
dialogManager.dismissAll();
dialogManager.show((setState, close) => CustomAlertDialog(
title: _msgBoxTitle(title),
content: content,
actions: buttons,
onCancel: hasCancel ? close : null,
));
}
2020-11-25 18:33:09 +08:00
Color str2color(String str, [alpha = 0xFF]) {
var hash = 160 << 16 + 114 << 8 + 91;
for (var i = 0; i < str.length; i += 1) {
hash = str.codeUnitAt(i) + ((hash << 5) - hash);
}
2021-08-14 14:14:01 +08:00
hash = hash % 16777216;
return Color((hash & 0xFF7FFF) | (alpha << 24));
2020-11-25 18:33:09 +08:00
}
2022-02-02 17:25:56 +08:00
const K = 1024;
const M = K * K;
const G = M * K;
String readableFileSize(double size) {
if (size < K) {
return "${size.toStringAsFixed(2)} B";
} else if (size < M) {
return "${(size / K).toStringAsFixed(2)} KB";
} else if (size < G) {
return "${(size / M).toStringAsFixed(2)} MB";
} else {
return "${(size / G).toStringAsFixed(2)} GB";
}
}
/// Flutter can't not catch PointerMoveEvent when size is 1
/// This will happen in Android AccessibilityService Input
/// android can't init dispatching size yet ,see: https://stackoverflow.com/questions/59960451/android-accessibility-dispatchgesture-is-it-possible-to-specify-pressure-for-a
/// use this temporary solution until flutter or android fixes the bug
class AccessibilityListener extends StatelessWidget {
final Widget? child;
static final offset = 100;
AccessibilityListener({this.child});
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: (evt) {
2022-05-26 18:25:16 +08:00
if (evt.size == 1) {
GestureBinding.instance.handlePointerEvent(PointerAddedEvent(
pointer: evt.pointer + offset, position: evt.position));
2022-05-26 18:25:16 +08:00
GestureBinding.instance.handlePointerEvent(PointerDownEvent(
pointer: evt.pointer + offset,
size: 0.1,
position: evt.position));
}
},
onPointerUp: (evt) {
if (evt.size == 1) {
2022-05-26 18:25:16 +08:00
GestureBinding.instance.handlePointerEvent(PointerUpEvent(
pointer: evt.pointer + offset,
size: 0.1,
position: evt.position));
2022-05-26 18:25:16 +08:00
GestureBinding.instance.handlePointerEvent(PointerRemovedEvent(
pointer: evt.pointer + offset, position: evt.position));
}
},
onPointerMove: (evt) {
if (evt.size == 1) {
2022-05-26 18:25:16 +08:00
GestureBinding.instance.handlePointerEvent(PointerMoveEvent(
pointer: evt.pointer + offset,
size: 0.1,
delta: evt.delta,
position: evt.position));
}
},
child: child);
}
}
2022-04-20 22:37:47 +08:00
class PermissionManager {
static Completer<bool>? _completer;
static Timer? _timer;
static var _current = "";
static final permissions = [
"audio",
"file",
"ignore_battery_optimizations",
"application_details_settings"
];
2022-04-20 22:37:47 +08:00
static bool isWaitingFile() {
if (_completer != null) {
return !_completer!.isCompleted && _current == "file";
}
return false;
}
static Future<bool> check(String type) {
if (isDesktop) {
return Future.value(true);
}
if (!permissions.contains(type)) {
2022-04-20 22:37:47 +08:00
return Future.error("Wrong permission!$type");
}
return gFFI.invokeMethod("check_permission", type);
2022-04-20 22:37:47 +08:00
}
static Future<bool> request(String type) {
if (isDesktop) {
return Future.value(true);
}
if (!permissions.contains(type)) {
2022-04-20 22:37:47 +08:00
return Future.error("Wrong permission!$type");
}
2022-04-20 22:37:47 +08:00
2022-08-01 14:33:08 +08:00
gFFI.invokeMethod("request_permission", type);
if (type == "ignore_battery_optimizations") {
return Future.value(false);
}
2022-04-20 22:37:47 +08:00
_current = type;
_completer = Completer<bool>();
gFFI.invokeMethod("request_permission", type);
2022-04-20 22:37:47 +08:00
// timeout
_timer?.cancel();
_timer = Timer(Duration(seconds: 60), () {
if (_completer == null) return;
if (!_completer!.isCompleted) {
_completer!.complete(false);
}
_completer = null;
_current = "";
});
return _completer!.future;
}
static complete(String type, bool res) {
if (type != _current) {
res = false;
}
_timer?.cancel();
_completer?.complete(res);
_current = "";
}
}
2022-07-29 18:34:25 +08:00
RadioListTile<T> getRadio<T>(
2022-09-27 22:56:18 +08:00
String name, T toValue, T curValue, void Function(T?) onChange,
{EdgeInsetsGeometry? contentPadding}) {
2022-07-29 18:34:25 +08:00
return RadioListTile<T>(
2022-09-27 22:56:18 +08:00
contentPadding: contentPadding,
2022-07-29 18:34:25 +08:00
controlAffinity: ListTileControlAffinity.trailing,
title: Text(translate(name)),
value: toValue,
groupValue: curValue,
onChanged: onChange,
dense: true,
);
}
2022-08-05 20:29:43 +08:00
CheckboxListTile getToggle(
String id, void Function(void Function()) setState, option, name,
{FFI? ffi}) {
2022-08-16 15:22:57 +08:00
final opt = bind.sessionGetToggleOptionSync(id: id, arg: option);
2022-08-05 20:29:43 +08:00
return CheckboxListTile(
value: opt,
onChanged: (v) {
setState(() {
bind.sessionToggleOption(id: id, value: option);
});
if (option == "show-quality-monitor") {
(ffi ?? gFFI).qualityMonitorModel.checkShowQualityMonitor(id);
2022-08-05 20:29:43 +08:00
}
},
dense: true,
title: Text(translate(name)));
}
/// find ffi, tag is Remote ID
/// for session specific usage
FFI ffi(String? tag) {
return Get.find<FFI>(tag: tag);
}
/// Global FFI object
late FFI _globalFFI;
FFI get gFFI => _globalFFI;
Future<void> initGlobalFFI() async {
2022-08-04 17:24:02 +08:00
debugPrint("_globalFFI init");
_globalFFI = FFI();
2022-08-04 17:24:02 +08:00
debugPrint("_globalFFI init end");
// after `put`, can also be globally found by Get.find<FFI>();
Get.put(_globalFFI, permanent: true);
}
String translate(String name) {
if (name.startsWith('Failed to') && name.contains(': ')) {
return name.split(': ').map((x) => translate(x)).join(': ');
}
return platformFFI.translate(name, localeName);
}
bool option2bool(String option, String value) {
bool res;
if (option.startsWith("enable-")) {
res = value != "N";
} else if (option.startsWith("allow-") ||
option == "stop-service" ||
option == "direct-server" ||
option == "stop-rendezvous-service" ||
option == "force-always-relay") {
res = value == "Y";
} else {
assert(false);
res = value != "N";
}
return res;
}
String bool2option(String option, bool b) {
String res;
if (option.startsWith('enable-')) {
res = b ? '' : 'N';
} else if (option.startsWith('allow-') ||
option == "stop-service" ||
option == "direct-server" ||
option == "stop-rendezvous-service" ||
option == "force-always-relay") {
res = b ? 'Y' : '';
} else {
assert(false);
res = b ? 'Y' : 'N';
}
return res;
}
Future<bool> matchPeer(String searchText, Peer peer) async {
if (searchText.isEmpty) {
return true;
}
if (peer.id.toLowerCase().contains(searchText)) {
return true;
}
if (peer.hostname.toLowerCase().contains(searchText) ||
peer.username.toLowerCase().contains(searchText)) {
return true;
}
final alias = await bind.mainGetPeerOption(id: peer.id, key: 'alias');
if (alias.isEmpty) {
return false;
}
return alias.toLowerCase().contains(searchText);
}
/// Get the image for the current [platform].
Widget getPlatformImage(String platform, {double size = 50}) {
platform = platform.toLowerCase();
if (platform == 'mac os') {
platform = 'mac';
} else if (platform != 'linux' && platform != 'android') {
platform = 'win';
}
2022-09-27 18:34:05 +08:00
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
}
class LastWindowPosition {
double? width;
double? height;
double? offsetWidth;
double? offsetHeight;
bool? isMaximized;
LastWindowPosition(this.width, this.height, this.offsetWidth,
this.offsetHeight, this.isMaximized);
Map<String, dynamic> toJson() {
return <String, dynamic>{
"width": width,
"height": height,
"offsetWidth": offsetWidth,
"offsetHeight": offsetHeight,
"isMaximized": isMaximized,
};
}
@override
String toString() {
return jsonEncode(toJson());
}
static LastWindowPosition? loadFromString(String content) {
if (content.isEmpty) {
return null;
}
try {
final m = jsonDecode(content);
return LastWindowPosition(m["width"], m["height"], m["offsetWidth"],
m["offsetHeight"], m["isMaximized"]);
} catch (e) {
debugPrintStack(label: e.toString());
return null;
}
}
}
/// Save window position and size on exit
/// Note that windowId must be provided if it's subwindow
Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
if (type != WindowType.Main && windowId == null) {
debugPrint(
"Error: windowId cannot be null when saving positions for sub window");
}
switch (type) {
case WindowType.Main:
final position = await windowManager.getPosition();
final sz = await windowManager.getSize();
final isMaximized = await windowManager.isMaximized();
final pos = LastWindowPosition(
sz.width, sz.height, position.dx, position.dy, isMaximized);
await bind.setLocalFlutterConfig(
k: kWindowPrefix + type.name, v: pos.toString());
break;
default:
final wc = WindowController.fromWindowId(windowId!);
final frame = await wc.getFrame();
final position = frame.topLeft;
final sz = frame.size;
final isMaximized = await wc.isMaximized();
final pos = LastWindowPosition(
sz.width, sz.height, position.dx, position.dy, isMaximized);
debugPrint(
"saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}");
await bind.setLocalFlutterConfig(
k: kWindowPrefix + type.name, v: pos.toString());
break;
}
}
Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
const double minWidth = 600;
const double minHeight = 100;
double maxWidth = (((isDesktop || isWebDesktop)
? kDesktopMaxDisplayWidth
: kMobileMaxDisplayWidth))
.toDouble();
double maxHeight = ((isDesktop || isWebDesktop)
? kDesktopMaxDisplayHeight
: kMobileMaxDisplayHeight)
.toDouble();
if (isDesktop || isWebDesktop) {
final screen = (await window_size.getWindowInfo()).screen;
if (screen != null) {
maxWidth = screen.visibleFrame.width;
maxHeight = screen.visibleFrame.height;
}
}
final defaultWidth =
((isDesktop || isWebDesktop) ? 1280 : kMobileDefaultDisplayWidth)
.toDouble();
final defaultHeight =
((isDesktop || isWebDesktop) ? 720 : kMobileDefaultDisplayHeight)
.toDouble();
double restoreWidth = width ?? defaultWidth;
double restoreHeight = height ?? defaultHeight;
if (restoreWidth < minWidth) {
restoreWidth = minWidth;
}
if (restoreHeight < minHeight) {
restoreHeight = minHeight;
}
if (restoreWidth > maxWidth) {
restoreWidth = maxWidth;
}
if (restoreHeight > maxHeight) {
2022-11-09 15:14:11 +08:00
restoreHeight = maxHeight;
}
return Size(restoreWidth, restoreHeight);
}
/// return null means center
Future<Offset?> _adjustRestoreMainWindowOffset(
double? left, double? top) async {
if (left == null || top == null) {
await windowManager.center();
} else {
2022-11-09 15:14:11 +08:00
double windowLeft = max(0.0, left);
double windowTop = max(0.0, top);
2022-11-09 15:14:11 +08:00
double frameLeft = double.infinity;
double frameTop = double.infinity;
double frameRight = ((isDesktop || isWebDesktop)
? kDesktopMaxDisplayWidth
: kMobileMaxDisplayWidth)
.toDouble();
double frameBottom = ((isDesktop || isWebDesktop)
? kDesktopMaxDisplayHeight
: kMobileMaxDisplayHeight)
.toDouble();
if (isDesktop || isWebDesktop) {
for (final screen in await window_size.getScreenList()) {
2022-11-09 15:14:11 +08:00
frameLeft = min(screen.visibleFrame.left, frameLeft);
frameTop = min(screen.visibleFrame.top, frameTop);
frameRight = max(screen.visibleFrame.right, frameRight);
frameBottom = max(screen.visibleFrame.bottom, frameBottom);
}
}
if (windowLeft < frameLeft ||
windowLeft > frameRight ||
windowTop < frameTop ||
windowTop > frameBottom) {
return null;
} else {
return Offset(windowLeft, windowTop);
}
}
return null;
}
/// Restore window position and size on start
/// Note that windowId must be provided if it's subwindow
Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
if (type != WindowType.Main && windowId == null) {
debugPrint(
"Error: windowId cannot be null when saving positions for sub window");
}
final pos = bind.getLocalFlutterConfig(k: kWindowPrefix + type.name);
var lpos = LastWindowPosition.loadFromString(pos);
if (lpos == null) {
debugPrint("no window position saved, ignoring position restoration");
return false;
}
switch (type) {
case WindowType.Main:
if (lpos.isMaximized == true) {
await windowManager.maximize();
} else {
final size =
await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
final offset = await _adjustRestoreMainWindowOffset(
lpos.offsetWidth, lpos.offsetHeight);
await windowManager.setSize(size);
if (offset == null) {
await windowManager.center();
} else {
await windowManager.setPosition(offset);
}
}
return true;
default:
final wc = WindowController.fromWindowId(windowId!);
if (lpos.isMaximized == true) {
await wc.maximize();
} else {
final size =
await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
final offset = await _adjustRestoreMainWindowOffset(
lpos.offsetWidth, lpos.offsetHeight);
debugPrint(
"restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}");
if (offset == null) {
await wc.center();
} else {
final frame =
Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
await wc.setFrame(frame);
}
}
break;
}
return false;
}
2022-10-18 10:29:33 +08:00
/// Initialize uni links for macos/windows
///
/// [Availability]
/// initUniLinks should only be used on macos/windows.
/// we use dbus for linux currently.
Future<void> initUniLinks() async {
if (!Platform.isWindows && !Platform.isMacOS) {
return;
}
if (Platform.isWindows) {
registerProtocol('rustdesk');
}
// check cold boot
try {
final initialLink = await getInitialLink();
if (initialLink == null) {
return;
}
parseRustdeskUri(initialLink);
} catch (err) {
debugPrintStack(label: "$err");
2022-10-18 10:29:33 +08:00
}
}
StreamSubscription? listenUniLinks() {
if (!(Platform.isWindows || Platform.isMacOS)) {
return null;
2022-10-18 10:29:33 +08:00
}
final sub = uriLinkStream.listen((Uri? uri) {
if (uri != null) {
callUniLinksUriHandler(uri);
} else {
print("uni listen error: uri is empty.");
}
}, onError: (err) {
print("uni links error: $err");
});
return sub;
2022-10-18 10:29:33 +08:00
}
/// Returns true if we successfully handle the startup arguments.
bool checkArguments() {
2022-10-11 19:52:03 +08:00
// check connect args
final connectIndex = bootArgs.indexOf("--connect");
if (connectIndex == -1) {
return false;
2022-10-11 19:52:03 +08:00
}
2022-10-12 21:57:19 +08:00
String? arg =
bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1];
if (arg != null) {
if (arg.startsWith(kUniLinksPrefix)) {
return parseRustdeskUri(arg);
2022-10-12 21:57:19 +08:00
} else {
// remove "--connect xxx" in the `bootArgs` array
2022-10-12 21:57:19 +08:00
bootArgs.removeAt(connectIndex);
bootArgs.removeAt(connectIndex);
// fallback to peer id
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(arg);
});
return true;
2022-10-12 21:57:19 +08:00
}
}
return false;
2022-10-12 21:57:19 +08:00
}
/// Parse `rustdesk://` unilinks
///
/// Returns true if we successfully handle the uri provided.
2022-10-12 21:57:19 +08:00
/// [Functions]
/// 1. New Connection: rustdesk://connection/new/your_peer_id
bool parseRustdeskUri(String uriPath) {
2022-10-12 21:57:19 +08:00
final uri = Uri.tryParse(uriPath);
if (uri == null) {
print("uri is not valid: $uriPath");
return false;
2022-10-12 21:57:19 +08:00
}
return callUniLinksUriHandler(uri);
2022-10-18 10:29:33 +08:00
}
/// uri handler
///
/// Returns true if we successfully handle the uri provided.
bool callUniLinksUriHandler(Uri uri) {
debugPrint("uni links called: $uri");
2022-10-12 21:57:19 +08:00
// new connection
if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
final peerId = uri.path.substring("/new/".length);
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(peerId);
});
return true;
2022-10-11 19:52:03 +08:00
}
return false;
2022-10-11 19:52:03 +08:00
}
connectMainDesktop(String id,
{required bool isFileTransfer,
required bool isTcpTunneling,
required bool isRDP}) async {
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP);
} else {
await rustDeskWinManager.newRemoteDesktop(id);
}
}
/// Connect to a peer with [id].
/// If [isFileTransfer], starts a session only for file transfer.
/// If [isTcpTunneling], starts a session only for tcp tunneling.
/// If [isRDP], starts a session only for rdp.
connect(BuildContext context, String id,
{bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false}) async {
if (id == '') return;
id = id.replaceAll(' ', '');
assert(!(isFileTransfer && isTcpTunneling && isRDP),
"more than one connect type");
if (isDesktop) {
if (desktopType == DesktopType.main) {
await connectMainDesktop(
id,
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
);
} else {
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id,
'isFileTransfer': isFileTransfer,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
});
}
} else {
if (isFileTransfer) {
if (!await PermissionManager.check("file")) {
if (!await PermissionManager.request("file")) {
return;
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FileManagerPage(id: id),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => RemotePage(id: id),
),
);
}
}
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
}
2022-09-27 17:52:36 +08:00
Future<Map<String, String>> getHttpHeaders() async {
return {
'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
2022-09-27 17:52:36 +08:00
};
}
// Simple wrapper of built-in types for refrence use.
class SimpleWrapper<T> {
T value;
SimpleWrapper(this.value);
}
/// call this to reload current window.
///
/// [Note]
/// Must have [RefreshWrapper] on the top of widget tree.
void reloadCurrentWindow() {
if (Get.context != null) {
// reload self window
RefreshWrapper.of(Get.context!)?.rebuild();
} else {
debugPrint(
"reload current window failed, global BuildContext does not exist");
}
}
/// call this to reload all windows, including main + all sub windows.
Future<void> reloadAllWindows() async {
reloadCurrentWindow();
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
for (final id in ids) {
DesktopMultiWindow.invokeMethod(id, kWindowActionRebuild);
}
} on AssertionError {
// ignore
}
}
2022-11-05 23:41:22 +08:00
/// Indicate the flutter app is running in portable mode.
///
/// [Note]
/// Portable build is only avaliable on Windows.
bool isRunningInPortableMode() {
if (!Platform.isWindows) {
return false;
}
return bool.hasEnvironment(kEnvPortableExecutable);
}
/// Window status callback
void onActiveWindowChanged() async {
print(
"[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}");
if (rustDeskWinManager.getActiveWindows().isEmpty) {
// close all sub windows
try {
await Future.wait([
saveWindowPosition(WindowType.Main),
rustDeskWinManager.closeAllSubWindows()
]);
} catch (err) {
debugPrintStack(label: "$err");
2022-11-05 23:41:22 +08:00
} finally {
await windowManager.setPreventClose(false);
await windowManager.close();
}
}
}
2022-11-29 22:36:35 +08:00
Timer periodic_immediate(Duration duration, Future<void> Function() callback) {
Future.delayed(Duration.zero, callback);
return Timer.periodic(duration, (timer) async {
await callback();
});
}
/// return a human readable windows version
WindowsTarget getWindowsTarget(int buildNumber) {
if (!Platform.isWindows) {
return WindowsTarget.naw;
}
if (buildNumber >= 22000) {
return WindowsTarget.w11;
} else if (buildNumber >= 10240) {
return WindowsTarget.w10;
} else if (buildNumber >= 9600) {
return WindowsTarget.w8_1;
} else if (buildNumber >= 9200) {
return WindowsTarget.w8;
} else if (buildNumber >= 7601) {
return WindowsTarget.w7;
} else if (buildNumber >= 6002) {
return WindowsTarget.vista;
} else {
// minimum support
return WindowsTarget.xp;
}
}
/// Get windows target build number.
///
/// [Note]
/// Please use this function wrapped with `Platform.isWindows`.
int getWindowsTargetBuildNumber() {
final rtlGetVersion = DynamicLibrary.open('ntdll.dll').lookupFunction<
Void Function(Pointer<win32.OSVERSIONINFOEX>),
void Function(Pointer<win32.OSVERSIONINFOEX>)>('RtlGetVersion');
final osVersionInfo = getOSVERSIONINFOEXPointer();
rtlGetVersion(osVersionInfo);
int buildNumber = osVersionInfo.ref.dwBuildNumber;
calloc.free(osVersionInfo);
return buildNumber;
}
/// Get Windows OS version pointer
///
/// [Note]
/// Please use this function wrapped with `Platform.isWindows`.
Pointer<win32.OSVERSIONINFOEX> getOSVERSIONINFOEXPointer() {
final pointer = calloc<win32.OSVERSIONINFOEX>();
pointer.ref
..dwOSVersionInfoSize = sizeOf<win32.OSVERSIONINFOEX>()
..dwBuildNumber = 0
..dwMajorVersion = 0
..dwMinorVersion = 0
..dwPlatformId = 0
..szCSDVersion = ''
..wServicePackMajor = 0
..wServicePackMinor = 0
..wSuiteMask = 0
..wProductType = 0
..wReserved = 0;
return pointer;
}
/// Indicating we need to use compatible ui mode.
///
/// [Conditions]
/// - Windows 7, window will overflow when we use frameless ui.
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() ?? '';
}
2022-12-21 15:24:01 +08:00
/// 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'] ?? '';
}
2022-12-21 15:24:01 +08:00
/// 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();
}
2022-12-21 15:24:01 +08:00
/// 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'] ?? "";
}