rustdesk/flutter/lib/common.dart
2023-05-08 12:34:19 +08:00

2047 lines
66 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:convert';
import 'dart:ffi' hide Size;
import 'dart:io';
import 'dart:math';
import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
import 'package:uni_links/uni_links.dart';
import 'package:uni_links_desktop/uni_links_desktop.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:win32/win32.dart' as win32;
import 'package:window_manager/window_manager.dart';
import 'package:window_size/window_size.dart' as window_size;
import '../consts.dart';
import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
import 'models/input_model.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
final globalKey = GlobalKey<NavigatorState>();
final navigationBarKey = GlobalKey();
final isAndroid = Platform.isAndroid;
final isIOS = Platform.isIOS;
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
var isWeb = false;
var isWebDesktop = false;
var isMobile = isAndroid || isIOS;
var version = "";
int androidVersion = 0;
/// Incriment count for textureId.
int _textureId = 0;
int get newTextureId => _textureId++;
final textureRenderer = TextureRgbaRenderer();
/// only available for Windows target
int windowsBuildNumber = 0;
DesktopType? desktopType;
/// Check if the app is running with single view mode.
bool isSingleViewApp() {
return desktopType == DesktopType.cm;
}
/// * debug or test only, DO NOT enable in release build
bool isTest = false;
typedef F = String Function(String);
typedef FMethod = String Function(String, dynamic);
typedef StreamEventHandler = Future<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.border2,
required this.highlight,
required this.drag_indicator,
required this.shadow,
});
final Color? border;
final Color? border2;
final Color? highlight;
final Color? drag_indicator;
final Color? shadow;
static final light = ColorThemeExtension(
border: Color(0xFFCCCCCC),
border2: Color(0xFFBBBBBB),
highlight: Color(0xFFE5E5E5),
drag_indicator: Colors.grey[800],
shadow: Colors.black,
);
static final dark = ColorThemeExtension(
border: Color(0xFF555555),
border2: Color(0xFFE5E5E5),
highlight: Color(0xFF3F3F3F),
drag_indicator: Colors.grey,
shadow: Colors.grey,
);
@override
ThemeExtension<ColorThemeExtension> copyWith({
Color? border,
Color? border2,
Color? highlight,
Color? drag_indicator,
Color? shadow,
}) {
return ColorThemeExtension(
border: border ?? this.border,
border2: border2 ?? this.border2,
highlight: highlight ?? this.highlight,
drag_indicator: drag_indicator ?? this.drag_indicator,
shadow: shadow ?? this.shadow,
);
}
@override
ThemeExtension<ColorThemeExtension> lerp(
ThemeExtension<ColorThemeExtension>? other, double t) {
if (other is! ColorThemeExtension) {
return this;
}
return ColorThemeExtension(
border: Color.lerp(border, other.border, t),
border2: Color.lerp(border2, other.border2, t),
highlight: Color.lerp(highlight, other.highlight, t),
drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t),
shadow: Color.lerp(shadow, other.shadow, t),
);
}
}
class MyTheme {
MyTheme._();
static const Color grayBg = Color(0xFFEFEFF2);
static const Color accent = Color(0xFF0071FF);
static const Color accent50 = Color(0x770071FF);
static const Color accent80 = Color(0xAA0071FF);
static const Color canvasColor = Color(0xFF212121);
static const Color border = Color(0xFFCCCCCC);
static const Color idColor = Color(0xFF00B6F0);
static const Color darkGray = Color.fromARGB(255, 148, 148, 148);
static const Color cmIdColor = Color(0xFF21790B);
static const Color dark = Colors.black87;
static const Color button = Color(0xFF2C8CFF);
static const Color hoverBorder = Color(0xFF999999);
// ListTile
static const ListTileThemeData listTileTheme = ListTileThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
);
// Checkbox
static const CheckboxThemeData checkboxTheme = CheckboxThemeData(
splashRadius: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
);
// TextButton
// Value is used to calculate "dialog.actionsPadding"
static const double mobileTextButtonPaddingLR = 20;
// TextButton on mobile needs a fixed padding, otherwise small buttons
// like "OK" has a larger left/right padding.
static TextButtonThemeData mobileTextButtonTheme = TextButtonThemeData(
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: mobileTextButtonPaddingLR),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
);
// Dialogs
static const double dialogPadding = 24;
// padding bottom depends on content (some dialogs has no content)
static EdgeInsets dialogTitlePadding({bool content = true}) {
final double p = dialogPadding;
return EdgeInsets.fromLTRB(p, p, p, content ? 0 : p);
}
// padding bottom depends on actions (mobile has dialogs without actions)
static EdgeInsets dialogContentPadding({bool actions = true}) {
final double p = dialogPadding;
return isDesktop
? EdgeInsets.fromLTRB(p, p, p, actions ? (p - 4) : p)
: EdgeInsets.fromLTRB(p, p, p, actions ? (p / 2) : p);
}
static EdgeInsets dialogActionsPadding() {
final double p = dialogPadding;
return isDesktop
? EdgeInsets.fromLTRB(p, 0, p, (p - 4))
: EdgeInsets.fromLTRB(p, 0, (p - mobileTextButtonPaddingLR), (p / 2));
}
static EdgeInsets dialogButtonPadding = isDesktop
? EdgeInsets.only(left: dialogPadding)
: EdgeInsets.only(left: dialogPadding / 3);
static ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
hoverColor: Color.fromARGB(255, 224, 224, 224),
scaffoldBackgroundColor: Colors.white,
dialogBackgroundColor: Colors.white,
dialogTheme: DialogTheme(
elevation: 15,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
side: BorderSide(
width: 1,
color: grayBg,
),
),
),
inputDecorationTheme: isDesktop
? InputDecorationTheme(
fillColor: grayBg,
filled: true,
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
)
: null,
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 19, color: Colors.black87),
titleSmall: TextStyle(fontSize: 14, color: Colors.black87),
bodySmall: TextStyle(fontSize: 12, color: Colors.black87, height: 1.25),
bodyMedium:
TextStyle(fontSize: 14, color: Colors.black87, height: 1.25),
labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)),
cardColor: grayBg,
hintColor: Color(0xFFAAAAAA),
visualDensity: VisualDensity.adaptivePlatformDensity,
tabBarTheme: const TabBarTheme(
labelColor: Colors.black87,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
splashFactory: isDesktop ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop
? TextButtonThemeData(
style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
),
),
)
: mobileTextButtonTheme,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: MyTheme.accent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
backgroundColor: grayBg,
foregroundColor: Colors.black87,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
checkboxTheme: checkboxTheme,
listTileTheme: listTileTheme,
menuBarTheme: MenuBarThemeData(
style:
MenuStyle(backgroundColor: MaterialStatePropertyAll(Colors.white))),
colorScheme: ColorScheme.light(
primary: Colors.blue, secondary: accent, background: grayBg),
).copyWith(
extensions: <ThemeExtension<dynamic>>[
ColorThemeExtension.light,
TabbarTheme.light,
],
);
static ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
hoverColor: Color.fromARGB(255, 45, 46, 53),
scaffoldBackgroundColor: Color(0xFF18191E),
dialogBackgroundColor: Color(0xFF18191E),
dialogTheme: DialogTheme(
elevation: 15,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
side: BorderSide(
width: 1,
color: Color(0xFF24252B),
),
),
),
inputDecorationTheme: isDesktop
? InputDecorationTheme(
fillColor: Color(0xFF24252B),
filled: true,
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
)
: null,
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 19),
titleSmall: TextStyle(fontSize: 14),
bodySmall: TextStyle(fontSize: 12, height: 1.25),
bodyMedium: TextStyle(fontSize: 14, height: 1.25),
labelLarge: TextStyle(
fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)),
cardColor: Color(0xFF24252B),
visualDensity: VisualDensity.adaptivePlatformDensity,
tabBarTheme: const TabBarTheme(
labelColor: Colors.white70,
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: MaterialStateProperty.all(Colors.grey[500]),
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
splashFactory: isDesktop ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop
? TextButtonThemeData(
style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
disabledForegroundColor: Colors.white70,
foregroundColor: Colors.white70,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
),
),
)
: mobileTextButtonTheme,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: MyTheme.accent,
foregroundColor: Colors.white,
disabledForegroundColor: Colors.white70,
disabledBackgroundColor: Colors.white10,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
backgroundColor: Color(0xFF24252B),
side: BorderSide(color: Colors.white12, width: 0.5),
disabledForegroundColor: Colors.white70,
foregroundColor: Colors.white70,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
checkboxTheme: checkboxTheme,
listTileTheme: listTileTheme,
menuBarTheme: MenuBarThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll(Color(0xFF121212)))),
colorScheme: ColorScheme.dark(
primary: Colors.blue,
secondary: accent,
background: Color(0xFF24252B),
),
).copyWith(
extensions: <ThemeExtension<dynamic>>[
ColorThemeExtension.dark,
TabbarTheme.dark,
],
);
static ThemeMode getThemeModePreference() {
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
}
static void changeDarkMode(ThemeMode mode) async {
Get.changeThemeMode(mode);
if (desktopType == DesktopType.main || isAndroid || isIOS) {
if (mode == ThemeMode.system) {
await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: '');
} else {
await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString());
}
await bind.mainChangeTheme(dark: mode.toShortString());
// Synchronize the window theme of the system.
updateSystemWindowTheme();
}
}
static ThemeMode currentThemeMode() {
final preference = getThemeModePreference();
if (preference == ThemeMode.system) {
if (WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.light) {
return ThemeMode.light;
} else {
return ThemeMode.dark;
}
} else {
return preference;
}
}
static ColorThemeExtension color(BuildContext context) {
return Theme.of(context).extension<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;
}
}
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
minimumSize: Size(0, 36),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2.0)),
),
);
List<Locale> supportedLocales = const [
Locale('en', 'US'),
Locale('zh', 'CN'),
Locale('zh', 'TW'),
Locale('zh', 'SG'),
Locale('fr'),
Locale('de'),
Locale('it'),
Locale('ja'),
Locale('cs'),
Locale('pl'),
Locale('ko'),
Locale('hu'),
Locale('pt'),
Locale('ru'),
Locale('sk'),
Locale('id'),
Locale('da'),
Locale('eo'),
Locale('tr'),
Locale('vi'),
Locale('pl'),
Locale('kz'),
Locale('es'),
];
String formatDurationToTime(Duration duration) {
var totalTime = duration.inSeconds;
final secs = totalTime % 60;
totalTime = (totalTime - secs) ~/ 60;
final mins = totalTime % 60;
totalTime = (totalTime - mins) ~/ 60;
return "${totalTime.toString().padLeft(2, "0")}:${mins.toString().padLeft(2, "0")}:${secs.toString().padLeft(2, "0")}";
}
closeConnection({String? id}) {
if (isAndroid || isIOS) {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
} else {
final controller = Get.find<DesktopTabController>();
controller.closeBy(id);
}
}
void window_on_top(int? id) {
if (!isDesktop) {
return;
}
if (id == null) {
// main window
windowManager.restore();
windowManager.show();
windowManager.focus();
rustDeskWinManager.registerActiveWindow(kWindowMainId);
} else {
WindowController.fromWindowId(id)
..focus()
..show();
rustDeskWinManager.call(WindowType.Main, kWindowEventShow, {"id": id});
}
}
typedef DialogBuilder = CustomAlertDialog Function(
StateSetter setState, void Function([dynamic]) close, BuildContext context);
class Dialog<T> {
OverlayEntry? entry;
Completer<T?> completer = Completer<T?>();
Dialog();
void complete(T? res) {
try {
if (!completer.isCompleted) {
completer.complete(res);
}
} catch (e) {
debugPrint("Dialog complete catch error: $e");
} finally {
entry?.remove();
}
}
}
class OverlayKeyState {
final _overlayKey = GlobalKey<OverlayState>();
/// use global overlay by default
OverlayState? get state =>
_overlayKey.currentState ?? globalKey.currentState?.overlay;
GlobalKey<OverlayState>? get key => _overlayKey;
}
class OverlayDialogManager {
final Map<String, Dialog> _dialogs = {};
var _overlayKeyState = OverlayKeyState();
int _tagCount = 0;
OverlayEntry? _mobileActionsOverlayEntry;
void setOverlayState(OverlayKeyState overlayKeyState) {
_overlayKeyState = overlayKeyState;
}
void dismissAll() {
_dialogs.forEach((key, value) {
value.complete(null);
BackButtonInterceptor.removeByName(key);
});
_dialogs.clear();
}
void dismissByTag(String tag) {
_dialogs[tag]?.complete(null);
_dialogs.remove(tag);
BackButtonInterceptor.removeByName(tag);
}
Future<T?> show<T>(DialogBuilder builder,
{bool clickMaskDismiss = false,
bool backDismiss = false,
String? tag,
bool useAnimation = true,
bool forceGlobal = false}) {
final overlayState =
forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state;
if (overlayState == null) {
return Future.error(
"[OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first");
}
final String dialogTag;
if (tag != null) {
dialogTag = tag;
} else {
dialogTag = _tagCount.toString();
_tagCount++;
}
final dialog = Dialog<T>();
_dialogs[dialogTag] = dialog;
close([res]) {
_dialogs.remove(dialogTag);
dialog.complete(res);
BackButtonInterceptor.removeByName(dialogTag);
}
dialog.entry = OverlayEntry(builder: (context) {
bool innerClicked = false;
return Listener(
onPointerUp: (_) {
if (!innerClicked && clickMaskDismiss) {
close();
}
innerClicked = false;
},
child: Container(
color: Theme.of(context).brightness == Brightness.light
? Colors.black12
: Colors.black45,
child: StatefulBuilder(builder: (context, setState) {
return Listener(
onPointerUp: (_) => innerClicked = true,
child: builder(setState, close, overlayState.context),
);
})));
});
overlayState.insert(dialog.entry!);
BackButtonInterceptor.add((stopDefaultButtonEvent, routeInfo) {
if (backDismiss) {
close();
}
return true;
}, name: dialogTag);
return dialog.completer.future;
}
String showLoading(String text,
{bool clickMaskDismiss = false,
bool showCancel = true,
VoidCallback? onCancel}) {
final tag = _tagCount.toString();
_tagCount++;
show((setState, close, context) {
cancel() {
dismissAll();
if (onCancel != null) {
onCancel();
}
}
return CustomAlertDialog(
content: Container(
constraints: const BoxConstraints(maxWidth: 240),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 30),
const Center(child: CircularProgressIndicator()),
const SizedBox(height: 20),
Center(
child: Text(translate(text),
style: const TextStyle(fontSize: 15))),
const SizedBox(height: 20),
Offstage(
offstage: !showCancel,
child: Center(
child: isDesktop
? dialogButton('Cancel', onPressed: cancel)
: TextButton(
style: flatButtonStyle,
onPressed: cancel,
child: Text(translate('Cancel'),
style: const TextStyle(
color: MyTheme.accent)))))
])),
onCancel: showCancel ? cancel : null,
);
}, tag: tag);
return tag;
}
void resetMobileActionsOverlay({FFI? ffi}) {
if (_mobileActionsOverlayEntry == null) return;
hideMobileActionsOverlay();
showMobileActionsOverlay(ffi: ffi);
}
void showMobileActionsOverlay({FFI? ffi}) {
if (_mobileActionsOverlayEntry != null) return;
final overlayState = _overlayKeyState.state;
if (overlayState == null) return;
// 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,
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
onRecentPressed: () async {
session.inputModel.sendMouse('down', MouseButtons.wheel);
await Future.delayed(const Duration(milliseconds: 500));
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();
}
}
bool existing(String tag) {
return _dialogs.keys.contains(tag);
}
}
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,
textAlign: TextAlign.center,
style: const TextStyle(
decoration: TextDecoration.none,
fontWeight: FontWeight.w300,
fontSize: 18,
color: Colors.white),
),
)));
});
overlayState.insert(entry);
Future.delayed(timeout, () {
entry.remove();
});
}
// TODO
// - Remove argument "contentPadding", no need for it, all should look the same.
// - Remove "required" for argument "content". See simple confirm dialog "delete peer", only title and actions are used. No need to "content: SizedBox.shrink()".
// - Make dead code alive, transform arguments "onSubmit" and "onCancel" into correspondenting buttons "ConfirmOkButton", "CancelButton".
class CustomAlertDialog extends StatelessWidget {
const CustomAlertDialog(
{Key? key,
this.title,
required this.content,
this.actions,
this.contentPadding,
this.contentBoxConstraints = const BoxConstraints(maxWidth: 500),
this.onSubmit,
this.onCancel})
: super(key: key);
final Widget? title;
final Widget content;
final List<Widget>? actions;
final double? contentPadding;
final BoxConstraints contentBoxConstraints;
final Function()? onSubmit;
final Function()? onCancel;
@override
Widget build(BuildContext context) {
// request focus
FocusScopeNode scopeNode = FocusScopeNode();
Future.delayed(Duration.zero, () {
if (!scopeNode.hasFocus) scopeNode.requestFocus();
});
bool tabTapped = false;
return FocusScope(
node: scopeNode,
autofocus: true,
onKey: (node, key) {
if (key.logicalKey == LogicalKeyboardKey.escape) {
if (key is RawKeyDownEvent) {
onCancel?.call();
}
return KeyEventResult.handled; // avoid TextField exception on escape
} else if (!tabTapped &&
onSubmit != null &&
key.logicalKey == LogicalKeyboardKey.enter) {
if (key is RawKeyDownEvent) onSubmit?.call();
return KeyEventResult.handled;
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
if (key is RawKeyDownEvent) {
scopeNode.nextFocus();
tabTapped = true;
}
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: AlertDialog(
scrollable: true,
title: title,
content: ConstrainedBox(
constraints: contentBoxConstraints,
child: content,
),
actions: actions,
titlePadding: MyTheme.dialogTitlePadding(content: content != null),
contentPadding:
MyTheme.dialogContentPadding(actions: actions is List),
actionsPadding: MyTheme.dialogActionsPadding(),
buttonPadding: MyTheme.dialogButtonPadding),
);
}
}
void msgBox(String id, String type, String title, String text, String link,
OverlayDialogManager dialogManager,
{bool? hasCancel, ReconnectHandle? reconnect}) {
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, dialogButton('OK', onPressed: submit));
}
hasCancel ??= !type.contains("error") &&
!type.contains("nocancel") &&
type != "restarting";
if (hasCancel) {
buttons.insert(
0, dialogButton('Cancel', onPressed: cancel, isOutline: true));
}
if (type.contains("hasclose")) {
buttons.insert(
0,
dialogButton('Close', onPressed: () {
dialogManager.dismissAll();
}));
}
if (reconnect != null && title == "Connection Error") {
buttons.insert(
0,
dialogButton('Reconnect', isOutline: true, onPressed: () {
reconnect(dialogManager, id, false);
}));
}
if (link.isNotEmpty) {
buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
}
dialogManager.show(
(setState, close, context) => CustomAlertDialog(
title: null,
content: SelectionArea(child: msgboxContent(type, title, text)),
actions: buttons,
onSubmit: hasOk ? submit : null,
onCancel: hasCancel == true ? cancel : null,
),
tag: '$id-$type-$title-$text-$link',
);
}
Color? _msgboxColor(String type) {
if (type == "input-password" || type == "custom-os-password") {
return Color(0xFFAD448E);
}
if (type.contains("success")) {
return Color(0xFF32bea6);
}
if (type.contains("error") || type == "re-input-password") {
return Color(0xFFE04F5F);
}
return Color(0xFF2C8CFF);
}
Widget msgboxIcon(String type) {
IconData? iconData;
if (type.contains("error") || type == "re-input-password") {
iconData = Icons.cancel;
}
if (type.contains("success")) {
iconData = Icons.check_circle;
}
if (type == "wait-uac" || type == "wait-remote-accept-nook") {
iconData = Icons.hourglass_top;
}
if (type == 'on-uac' || type == 'on-foreground-elevated') {
iconData = Icons.admin_panel_settings;
}
if (type == "info") {
iconData = Icons.info;
}
if (iconData != null) {
return Icon(iconData, size: 50, color: _msgboxColor(type))
.marginOnly(right: 16);
}
return Offstage();
}
// title should be null
Widget msgboxContent(String type, String title, String text) {
return Row(
children: [
msgboxIcon(type),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
translate(title),
style: TextStyle(fontSize: 21),
).marginOnly(bottom: 10),
Text(translate(text), style: const TextStyle(fontSize: 15)),
],
),
),
],
).marginOnly(bottom: 12);
}
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
Widget content, List<Widget> buttons,
{bool hasCancel = true}) {
dialogManager.show((setState, close, context) => CustomAlertDialog(
title: Text(
translate(title),
style: TextStyle(fontSize: 21),
),
content: content,
actions: buttons,
onCancel: hasCancel ? close : null,
));
}
Color str2color(String str, [alpha = 0xFF]) {
var hash = 160 << 16 + 114 << 8 + 91;
for (var i = 0; i < str.length; i += 1) {
hash = str.codeUnitAt(i) + ((hash << 5) - hash);
}
hash = hash % 16777216;
return Color((hash & 0xFF7FFF) | (alpha << 24));
}
const K = 1024;
const M = K * K;
const G = M * K;
String readableFileSize(double size) {
if (size < K) {
return "${size.toStringAsFixed(2)} B";
} else if (size < M) {
return "${(size / K).toStringAsFixed(2)} KB";
} else if (size < G) {
return "${(size / M).toStringAsFixed(2)} MB";
} else {
return "${(size / G).toStringAsFixed(2)} GB";
}
}
/// Flutter can't not catch PointerMoveEvent when size is 1
/// This will happen in Android AccessibilityService Input
/// android can't init dispatching size yet ,see: https://stackoverflow.com/questions/59960451/android-accessibility-dispatchgesture-is-it-possible-to-specify-pressure-for-a
/// use this temporary solution until flutter or android fixes the bug
class AccessibilityListener extends StatelessWidget {
final Widget? child;
static final offset = 100;
AccessibilityListener({this.child});
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: (evt) {
if (evt.size == 1) {
GestureBinding.instance.handlePointerEvent(PointerAddedEvent(
pointer: evt.pointer + offset, position: evt.position));
GestureBinding.instance.handlePointerEvent(PointerDownEvent(
pointer: evt.pointer + offset,
size: 0.1,
position: evt.position));
}
},
onPointerUp: (evt) {
if (evt.size == 1) {
GestureBinding.instance.handlePointerEvent(PointerUpEvent(
pointer: evt.pointer + offset,
size: 0.1,
position: evt.position));
GestureBinding.instance.handlePointerEvent(PointerRemovedEvent(
pointer: evt.pointer + offset, position: evt.position));
}
},
onPointerMove: (evt) {
if (evt.size == 1) {
GestureBinding.instance.handlePointerEvent(PointerMoveEvent(
pointer: evt.pointer + offset,
size: 0.1,
delta: evt.delta,
position: evt.position));
}
},
child: child);
}
}
class AndroidPermissionManager {
static Completer<bool>? _completer;
static Timer? _timer;
static var _current = "";
static bool isWaitingFile() {
if (_completer != null) {
return !_completer!.isCompleted && _current == kManageExternalStorage;
}
return false;
}
static Future<bool> check(String type) {
if (isDesktop) {
return Future.value(true);
}
return gFFI.invokeMethod("check_permission", type);
}
// startActivity goto Android Setting's page to request permission manually by user
static void startAction(String action) {
gFFI.invokeMethod(AndroidChannel.kStartAction, action);
}
/// We use XXPermissions to request permissions,
/// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java
static Future<bool> request(String type) {
if (isDesktop) {
return Future.value(true);
}
gFFI.invokeMethod("request_permission", type);
// clear last task
if (_completer?.isCompleted == false) {
_completer?.complete(false);
}
_timer?.cancel();
_current = type;
_completer = Completer<bool>();
_timer = Timer(Duration(seconds: 120), () {
if (_completer == null) return;
if (!_completer!.isCompleted) {
_completer!.complete(false);
}
_completer = null;
_current = "";
});
return _completer!.future;
}
static complete(String type, bool res) {
if (type != _current) {
res = false;
}
_timer?.cancel();
_completer?.complete(res);
_current = "";
}
}
// TODO move this to mobile/widgets.
// Used only for mobile, pages remote, settings, dialog
// TODO remove argument contentPadding, its not used, getToggle() has not
RadioListTile<T> getRadio<T>(
Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
{EdgeInsetsGeometry? contentPadding}) {
return RadioListTile<T>(
contentPadding: contentPadding ?? EdgeInsets.zero,
visualDensity: VisualDensity.compact,
controlAffinity: ListTileControlAffinity.trailing,
title: title,
value: toValue,
groupValue: curValue,
onChanged: onChange,
);
}
/// 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 {
debugPrint("_globalFFI init");
_globalFFI = FFI();
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}) {
if (platform == kPeerPlatformMacOS) {
platform = 'mac';
} else if (platform != kPeerPlatformLinux &&
platform != kPeerPlatformAndroid) {
platform = 'win';
} else {
platform = platform.toLowerCase();
}
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
}
class LastWindowPosition {
double? width;
double? height;
double? offsetWidth;
double? offsetHeight;
bool? isMaximized;
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) {
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 {
double windowLeft = max(0.0, left);
double windowTop = max(0.0, top);
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()) {
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;
}
/// Initialize uni links for macos/windows
///
/// [Availability]
/// initUniLinks should only be used on macos/windows.
/// we use dbus for linux currently.
Future<bool> initUniLinks() async {
if (Platform.isLinux) {
return false;
}
// Register uni links for Windows. The required info of url scheme is already
// declared in `Info.plist` for macOS.
if (Platform.isWindows) {
registerProtocol('rustdesk');
}
// check cold boot
try {
final initialLink = await getInitialLink();
if (initialLink == null) {
return false;
}
return parseRustdeskUri(initialLink);
} catch (err) {
debugPrintStack(label: "$err");
return false;
}
}
/// Listen for uni links.
///
/// * handleByFlutter: Should uni links be handled by Flutter.
///
/// Returns a [StreamSubscription] which can listen the uni links.
StreamSubscription? listenUniLinks({handleByFlutter = true}) {
if (Platform.isLinux) {
return null;
}
final sub = uriLinkStream.listen((Uri? uri) {
debugPrint("A uri was received: $uri.");
if (uri != null) {
if (handleByFlutter) {
callUniLinksUriHandler(uri);
} else {
bind.sendUrlScheme(url: uri.toString());
}
} else {
print("uni listen error: uri is empty.");
}
}, onError: (err) {
print("uni links error: $err");
});
return sub;
}
/// Handle command line arguments
///
/// * Returns true if we successfully handle the startup arguments.
bool checkArguments() {
if (kBootArgs.isNotEmpty) {
final ret = parseRustdeskUri(kBootArgs.first);
if (ret) {
return true;
}
}
// bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05]
// check connect args
var connectIndex = kBootArgs.indexOf("--connect");
if (connectIndex == -1) {
return false;
}
String? id =
kBootArgs.length <= connectIndex + 1 ? null : kBootArgs[connectIndex + 1];
String? password =
kBootArgs.length <= connectIndex + 2 ? null : kBootArgs[connectIndex + 2];
if (password != null && password.startsWith("--")) {
password = null;
}
final switchUuidIndex = kBootArgs.indexOf("--switch_uuid");
String? switchUuid = kBootArgs.length <= switchUuidIndex + 1
? null
: kBootArgs[switchUuidIndex + 1];
if (id != null) {
if (id.startsWith(kUniLinksPrefix)) {
return parseRustdeskUri(id);
} else {
// remove "--connect xxx" in the `bootArgs` array
kBootArgs.removeAt(connectIndex);
kBootArgs.removeAt(connectIndex);
// fallback to peer id
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(id,
password: password, switch_uuid: switchUuid);
});
return true;
}
}
return false;
}
/// Parse `rustdesk://` unilinks
///
/// Returns true if we successfully handle the uri provided.
/// [Functions]
/// 1. New Connection: rustdesk://connection/new/your_peer_id
bool parseRustdeskUri(String uriPath) {
final uri = Uri.tryParse(uriPath);
if (uri == null) {
debugPrint("uri is not valid: $uriPath");
return false;
}
return callUniLinksUriHandler(uri);
}
/// uri handler
///
/// Returns true if we successfully handle the uri provided.
bool callUniLinksUriHandler(Uri uri) {
debugPrint("uni links called: $uri");
// new connection
if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
final peerId = uri.path.substring("/new/".length);
var param = uri.queryParameters;
String? switch_uuid = param["switch_uuid"];
String? password = param["password"];
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(peerId,
password: password, switch_uuid: switch_uuid);
});
return true;
}
return false;
}
connectMainDesktop(String id,
{required bool isFileTransfer,
required bool isTcpTunneling,
required bool isRDP,
bool? forceRelay}) async {
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay);
} else {
await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay);
}
}
/// 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(' ', '');
final oldId = id;
id = await bind.mainHandleRelayId(id: id);
final forceRelay = id != oldId;
assert(!(isFileTransfer && isTcpTunneling && isRDP),
"more than one connect type");
if (isDesktop) {
if (desktopType == DesktopType.main) {
await connectMainDesktop(id,
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
forceRelay: forceRelay);
} else {
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id,
'isFileTransfer': isFileTransfer,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
"forceRelay": forceRelay,
});
}
} else {
if (isFileTransfer) {
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
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();
}
}
Map<String, String> getHttpHeaders() {
return {
'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
};
}
// Simple wrapper of built-in types for reference 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
}
}
/// Indicate the flutter app is running in portable mode.
///
/// [Note]
/// Portable build is only available on Windows.
bool isRunningInPortableMode() {
if (!Platform.isWindows) {
return false;
}
return bool.hasEnvironment(kEnvPortableExecutable);
}
/// Window status callback
Future<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");
} finally {
debugPrint("Start closing RustDesk...");
await windowManager.setPreventClose(false);
await windowManager.close();
if (Platform.isMacOS) {
RdPlatformChannel.instance.terminate();
}
}
}
}
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() ?? '';
}
/// decode from shared string (from user shared or rustdesk-server generated)
/// also see [encode]
/// throw when decoding failure
ServerConfig.decode(String msg) {
var json = {};
try {
// back compatible
json = jsonDecode(msg);
} catch (err) {
final input = msg.split('').reversed.join('');
final bytes = base64Decode(base64.normalize(input));
json = jsonDecode(utf8.decode(bytes));
}
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'] ?? "";
}
Widget dialogButton(String text,
{required VoidCallback? onPressed,
bool isOutline = false,
Widget? icon,
TextStyle? style,
ButtonStyle? buttonStyle}) {
if (isDesktop) {
if (isOutline) {
return icon == null
? OutlinedButton(
onPressed: onPressed,
child: Text(translate(text), style: style),
)
: OutlinedButton.icon(
icon: icon,
onPressed: onPressed,
label: Text(translate(text), style: style),
);
} else {
return icon == null
? ElevatedButton(
style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
onPressed: onPressed,
child: Text(translate(text), style: style),
)
: ElevatedButton.icon(
icon: icon,
style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
onPressed: onPressed,
label: Text(translate(text), style: style),
);
}
} else {
return TextButton(
onPressed: onPressed,
child: Text(
translate(text),
style: style,
),
);
}
}
int version_cmp(String v1, String v2) {
return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2);
}
String getWindowName({WindowType? overrideType}) {
switch (overrideType ?? kWindowType) {
case WindowType.Main:
return "RustDesk";
case WindowType.FileTransfer:
return "File Transfer - RustDesk";
case WindowType.PortForward:
return "Port Forward - RustDesk";
case WindowType.RemoteDesktop:
return "Remote Desktop - RustDesk";
default:
break;
}
return "RustDesk";
}
String getWindowNameWithId(String id, {WindowType? overrideType}) {
return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}";
}
Future<void> updateSystemWindowTheme() async {
// Set system window theme for macOS.
final userPreference = MyTheme.getThemeModePreference();
if (userPreference != ThemeMode.system) {
if (Platform.isMacOS) {
await RdPlatformChannel.instance.changeSystemWindowTheme(
userPreference == ThemeMode.light
? SystemWindowTheme.light
: SystemWindowTheme.dark);
}
}
}
/// macOS only
///
/// Note: not found a general solution for rust based AVFoundation bingding.
/// [AVFoundation] crate has compile error.
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
enum PermissionAuthorizeType {
undetermined,
authorized,
denied, // and restricted
}
Future<PermissionAuthorizeType> osxCanRecordAudio() async {
int res = await kMacOSPermChannel.invokeMethod("canRecordAudio");
print(res);
if (res > 0) {
return PermissionAuthorizeType.authorized;
} else if (res == 0) {
return PermissionAuthorizeType.undetermined;
} else {
return PermissionAuthorizeType.denied;
}
}
Future<bool> osxRequestAudio() async {
return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
}
class DraggableNeverScrollableScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that does not let the user scroll.
const DraggableNeverScrollableScrollPhysics({super.parent});
@override
DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor));
}
@override
bool shouldAcceptUserOffset(ScrollMetrics position) {
// TODO: find a better solution to check if the offset change is caused by the scrollbar.
// Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity].
if (position is ScrollPositionWithSingleContext) {
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
return position.activity is IdleScrollActivity;
}
return false;
}
@override
bool get allowImplicitScrolling => false;
}
Widget futureBuilder(
{required Future? future, required Widget Function(dynamic data) hasData}) {
return FutureBuilder(
future: future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return hasData(snapshot.data!);
} else {
if (snapshot.hasError) {
debugPrint(snapshot.error.toString());
}
return Container();
}
});
}
void onCopyFingerprint(String value) {
if (value.isNotEmpty) {
Clipboard.setData(ClipboardData(text: value));
showToast('$value\n${translate("Copied")}');
} else {
showToast(translate("no fingerprints"));
}
}