508 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			508 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:async';
 | |
| import 'dart:convert';
 | |
| import 'dart:io';
 | |
| 
 | |
| import 'package:bot_toast/bot_toast.dart';
 | |
| import 'package:desktop_multi_window/desktop_multi_window.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter/services.dart';
 | |
| import 'package:flutter_hbb/common/widgets/overlay.dart';
 | |
| import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
 | |
| import 'package:flutter_hbb/desktop/pages/install_page.dart';
 | |
| import 'package:flutter_hbb/desktop/pages/server_page.dart';
 | |
| import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
 | |
| import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
 | |
| import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
 | |
| import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
 | |
| import 'package:flutter_hbb/models/state_model.dart';
 | |
| import 'package:flutter_hbb/utils/multi_window_manager.dart';
 | |
| import 'package:flutter_localizations/flutter_localizations.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:provider/provider.dart';
 | |
| import 'package:window_manager/window_manager.dart';
 | |
| 
 | |
| import 'common.dart';
 | |
| import 'consts.dart';
 | |
| import 'mobile/pages/home_page.dart';
 | |
| import 'mobile/pages/server_page.dart';
 | |
| import 'models/platform_model.dart';
 | |
| 
 | |
| import 'package:flutter_hbb/plugin/handlers.dart'
 | |
|     if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
 | |
| 
 | |
| /// Basic window and launch properties.
 | |
| int? kWindowId;
 | |
| WindowType? kWindowType;
 | |
| late List<String> kBootArgs;
 | |
| 
 | |
| Future<void> main(List<String> args) async {
 | |
|   WidgetsFlutterBinding.ensureInitialized();
 | |
| 
 | |
|   debugPrint("launch args: $args");
 | |
|   kBootArgs = List.from(args);
 | |
| 
 | |
|   if (!isDesktop) {
 | |
|     runMobileApp();
 | |
|     return;
 | |
|   }
 | |
|   // main window
 | |
|   if (args.isNotEmpty && args.first == 'multi_window') {
 | |
|     kWindowId = int.parse(args[1]);
 | |
|     stateGlobal.setWindowId(kWindowId!);
 | |
|     if (!isMacOS) {
 | |
|       WindowController.fromWindowId(kWindowId!).showTitleBar(false);
 | |
|     }
 | |
|     final argument = args[2].isEmpty
 | |
|         ? <String, dynamic>{}
 | |
|         : jsonDecode(args[2]) as Map<String, dynamic>;
 | |
|     int type = argument['type'] ?? -1;
 | |
|     // to-do: No need to parse window id ?
 | |
|     // Because stateGlobal.windowId is a global value.
 | |
|     argument['windowId'] = kWindowId;
 | |
|     kWindowType = type.windowType;
 | |
|     switch (kWindowType) {
 | |
|       case WindowType.RemoteDesktop:
 | |
|         desktopType = DesktopType.remote;
 | |
|         runMultiWindow(
 | |
|           argument,
 | |
|           kAppTypeDesktopRemote,
 | |
|         );
 | |
|         break;
 | |
|       case WindowType.FileTransfer:
 | |
|         desktopType = DesktopType.fileTransfer;
 | |
|         runMultiWindow(
 | |
|           argument,
 | |
|           kAppTypeDesktopFileTransfer,
 | |
|         );
 | |
|         break;
 | |
|       case WindowType.PortForward:
 | |
|         desktopType = DesktopType.portForward;
 | |
|         runMultiWindow(
 | |
|           argument,
 | |
|           kAppTypeDesktopPortForward,
 | |
|         );
 | |
|         break;
 | |
|       default:
 | |
|         break;
 | |
|     }
 | |
|   } else if (args.isNotEmpty && args.first == '--cm') {
 | |
|     debugPrint("--cm started");
 | |
|     desktopType = DesktopType.cm;
 | |
|     await windowManager.ensureInitialized();
 | |
|     runConnectionManagerScreen();
 | |
|   } else if (args.contains('--install')) {
 | |
|     runInstallPage();
 | |
|   } else {
 | |
|     desktopType = DesktopType.main;
 | |
|     await windowManager.ensureInitialized();
 | |
|     windowManager.setPreventClose(true);
 | |
|     if (isMacOS) {
 | |
|       disableWindowMovable(kWindowId);
 | |
|     }
 | |
|     runMainApp(true);
 | |
|   }
 | |
| }
 | |
| 
 | |
| Future<void> initEnv(String appType) async {
 | |
|   // global shared preference
 | |
|   await platformFFI.init(appType);
 | |
|   // global FFI, use this **ONLY** for global configuration
 | |
|   // for convenience, use global FFI on mobile platform
 | |
|   // focus on multi-ffi on desktop first
 | |
|   await initGlobalFFI();
 | |
|   // await Firebase.initializeApp();
 | |
|   _registerEventHandler();
 | |
|   // Update the system theme.
 | |
|   updateSystemWindowTheme();
 | |
| }
 | |
| 
 | |
| void runMainApp(bool startService) async {
 | |
|   // register uni links
 | |
|   await initEnv(kAppTypeMain);
 | |
|   // trigger connection status updater
 | |
|   await bind.mainCheckConnectStatus();
 | |
|   if (startService) {
 | |
|     gFFI.serverModel.startService();
 | |
|     bind.pluginSyncUi(syncTo: kAppTypeMain);
 | |
|     bind.pluginListReload();
 | |
|   }
 | |
|   await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
 | |
|   gFFI.userModel.refreshCurrentUser();
 | |
|   runApp(App());
 | |
| 
 | |
|   // Set window option.
 | |
|   WindowOptions windowOptions = getHiddenTitleBarWindowOptions();
 | |
|   windowManager.waitUntilReadyToShow(windowOptions, () async {
 | |
|     // Restore the location of the main window before window hide or show.
 | |
|     await restoreWindowPosition(WindowType.Main);
 | |
|     // Check the startup argument, if we successfully handle the argument, we keep the main window hidden.
 | |
|     final handledByUniLinks = await initUniLinks();
 | |
|     debugPrint("handled by uni links: $handledByUniLinks");
 | |
|     if (handledByUniLinks || handleUriLink(cmdArgs: kBootArgs)) {
 | |
|       windowManager.hide();
 | |
|     } else {
 | |
|       windowManager.show();
 | |
|       windowManager.focus();
 | |
|       // Move registration of active main window here to prevent from async visible check.
 | |
|       rustDeskWinManager.registerActiveWindow(kWindowMainId);
 | |
|     }
 | |
|     windowManager.setOpacity(1);
 | |
|     windowManager.setTitle(getWindowName());
 | |
|     // Do not use `windowManager.setResizable()` here.
 | |
|     setResizable(!bind.isIncomingOnly());
 | |
|   });
 | |
| }
 | |
| 
 | |
| void runMobileApp() async {
 | |
|   await initEnv(kAppTypeMain);
 | |
|   if (isAndroid) androidChannelInit();
 | |
|   if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
 | |
|   draggablePositions.load();
 | |
|   await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
 | |
|   gFFI.userModel.refreshCurrentUser();
 | |
|   runApp(App());
 | |
|   if (!isWeb) await initUniLinks();
 | |
| }
 | |
| 
 | |
| void runMultiWindow(
 | |
|   Map<String, dynamic> argument,
 | |
|   String appType,
 | |
| ) async {
 | |
|   await initEnv(appType);
 | |
|   final title = getWindowName();
 | |
|   // set prevent close to true, we handle close event manually
 | |
|   WindowController.fromWindowId(kWindowId!).setPreventClose(true);
 | |
|   if (isMacOS) {
 | |
|     disableWindowMovable(kWindowId);
 | |
|   }
 | |
|   late Widget widget;
 | |
|   switch (appType) {
 | |
|     case kAppTypeDesktopRemote:
 | |
|       draggablePositions.load();
 | |
|       widget = DesktopRemoteScreen(
 | |
|         params: argument,
 | |
|       );
 | |
|       break;
 | |
|     case kAppTypeDesktopFileTransfer:
 | |
|       widget = DesktopFileTransferScreen(
 | |
|         params: argument,
 | |
|       );
 | |
|       break;
 | |
|     case kAppTypeDesktopPortForward:
 | |
|       widget = DesktopPortForwardScreen(
 | |
|         params: argument,
 | |
|       );
 | |
|       break;
 | |
|     default:
 | |
|       // no such appType
 | |
|       exit(0);
 | |
|   }
 | |
|   _runApp(
 | |
|     title,
 | |
|     widget,
 | |
|     MyTheme.currentThemeMode(),
 | |
|   );
 | |
|   // we do not hide titlebar on win7 because of the frame overflow.
 | |
|   if (kUseCompatibleUiMode) {
 | |
|     WindowController.fromWindowId(kWindowId!).showTitleBar(true);
 | |
|   }
 | |
|   switch (appType) {
 | |
|     case kAppTypeDesktopRemote:
 | |
|       // If screen rect is set, the window will be moved to the target screen and then set fullscreen.
 | |
|       if (argument['screen_rect'] == null) {
 | |
|         // display can be used to control the offset of the window.
 | |
|         await restoreWindowPosition(
 | |
|           WindowType.RemoteDesktop,
 | |
|           windowId: kWindowId!,
 | |
|           peerId: argument['id'] as String?,
 | |
|           display: argument['display'] as int?,
 | |
|         );
 | |
|       }
 | |
|       break;
 | |
|     case kAppTypeDesktopFileTransfer:
 | |
|       await restoreWindowPosition(WindowType.FileTransfer,
 | |
|           windowId: kWindowId!);
 | |
|       break;
 | |
|     case kAppTypeDesktopPortForward:
 | |
|       await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!);
 | |
|       break;
 | |
|     default:
 | |
|       // no such appType
 | |
|       exit(0);
 | |
|   }
 | |
|   // show window from hidden status
 | |
|   WindowController.fromWindowId(kWindowId!).show();
 | |
| }
 | |
| 
 | |
| void runConnectionManagerScreen() async {
 | |
|   await initEnv(kAppTypeConnectionManager);
 | |
|   _runApp(
 | |
|     '',
 | |
|     const DesktopServerPage(),
 | |
|     MyTheme.currentThemeMode(),
 | |
|   );
 | |
|   final hide = await bind.cmGetConfig(name: "hide_cm") == 'true';
 | |
|   gFFI.serverModel.hideCm = hide;
 | |
|   if (hide) {
 | |
|     await hideCmWindow(isStartup: true);
 | |
|   } else {
 | |
|     await showCmWindow(isStartup: true);
 | |
|   }
 | |
|   setResizable(false);
 | |
|   // Start the uni links handler and redirect links to Native, not for Flutter.
 | |
|   listenUniLinks(handleByFlutter: false);
 | |
| }
 | |
| 
 | |
| bool _isCmReadyToShow = false;
 | |
| 
 | |
| showCmWindow({bool isStartup = false}) async {
 | |
|   if (isStartup) {
 | |
|     WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
 | |
|         size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
 | |
|     await windowManager.waitUntilReadyToShow(windowOptions, null);
 | |
|     bind.mainHideDocker();
 | |
|     await Future.wait([
 | |
|       windowManager.show(),
 | |
|       windowManager.focus(),
 | |
|       windowManager.setOpacity(1)
 | |
|     ]);
 | |
|     // ensure initial window size to be changed
 | |
|     await windowManager.setSizeAlignment(
 | |
|         kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
 | |
|     _isCmReadyToShow = true;
 | |
|   } else if (_isCmReadyToShow) {
 | |
|     if (await windowManager.getOpacity() != 1) {
 | |
|       await windowManager.setOpacity(1);
 | |
|       await windowManager.focus();
 | |
|       await windowManager.minimize(); //needed
 | |
|       await windowManager.setSizeAlignment(
 | |
|           kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
 | |
|       windowOnTop(null);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| hideCmWindow({bool isStartup = false}) async {
 | |
|   if (isStartup) {
 | |
|     WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
 | |
|         size: kConnectionManagerWindowSizeClosedChat);
 | |
|     windowManager.setOpacity(0);
 | |
|     await windowManager.waitUntilReadyToShow(windowOptions, null);
 | |
|     bind.mainHideDocker();
 | |
|     await windowManager.minimize();
 | |
|     await windowManager.hide();
 | |
|     _isCmReadyToShow = true;
 | |
|   } else if (_isCmReadyToShow) {
 | |
|     if (await windowManager.getOpacity() != 0) {
 | |
|       await windowManager.setOpacity(0);
 | |
|       bind.mainHideDocker();
 | |
|       await windowManager.minimize();
 | |
|       await windowManager.hide();
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| void _runApp(
 | |
|   String title,
 | |
|   Widget home,
 | |
|   ThemeMode themeMode,
 | |
| ) {
 | |
|   final botToastBuilder = BotToastInit();
 | |
|   runApp(RefreshWrapper(
 | |
|     builder: (context) => GetMaterialApp(
 | |
|       navigatorKey: globalKey,
 | |
|       debugShowCheckedModeBanner: false,
 | |
|       title: title,
 | |
|       theme: MyTheme.lightTheme,
 | |
|       darkTheme: MyTheme.darkTheme,
 | |
|       themeMode: themeMode,
 | |
|       home: home,
 | |
|       localizationsDelegates: const [
 | |
|         GlobalMaterialLocalizations.delegate,
 | |
|         GlobalWidgetsLocalizations.delegate,
 | |
|         GlobalCupertinoLocalizations.delegate,
 | |
|       ],
 | |
|       supportedLocales: supportedLocales,
 | |
|       navigatorObservers: [
 | |
|         // FirebaseAnalyticsObserver(analytics: analytics),
 | |
|         BotToastNavigatorObserver(),
 | |
|       ],
 | |
|       builder: (context, child) {
 | |
|         child = _keepScaleBuilder(context, child);
 | |
|         child = botToastBuilder(context, child);
 | |
|         return child;
 | |
|       },
 | |
|     ),
 | |
|   ));
 | |
| }
 | |
| 
 | |
| void runInstallPage() async {
 | |
|   await windowManager.ensureInitialized();
 | |
|   await initEnv(kAppTypeMain);
 | |
|   _runApp('', const InstallPage(), MyTheme.currentThemeMode());
 | |
|   WindowOptions windowOptions =
 | |
|       getHiddenTitleBarWindowOptions(size: Size(800, 600), center: true);
 | |
|   windowManager.waitUntilReadyToShow(windowOptions, () async {
 | |
|     windowManager.show();
 | |
|     windowManager.focus();
 | |
|     windowManager.setOpacity(1);
 | |
|     windowManager.setAlignment(Alignment.center); // ensure
 | |
|   });
 | |
| }
 | |
| 
 | |
| WindowOptions getHiddenTitleBarWindowOptions(
 | |
|     {Size? size, bool center = false, bool? alwaysOnTop}) {
 | |
|   var defaultTitleBarStyle = TitleBarStyle.hidden;
 | |
|   // we do not hide titlebar on win7 because of the frame overflow.
 | |
|   if (kUseCompatibleUiMode) {
 | |
|     defaultTitleBarStyle = TitleBarStyle.normal;
 | |
|   }
 | |
|   return WindowOptions(
 | |
|     size: size,
 | |
|     center: center,
 | |
|     backgroundColor: Colors.transparent,
 | |
|     skipTaskbar: false,
 | |
|     titleBarStyle: defaultTitleBarStyle,
 | |
|     alwaysOnTop: alwaysOnTop,
 | |
|   );
 | |
| }
 | |
| 
 | |
| class App extends StatefulWidget {
 | |
|   @override
 | |
|   State<App> createState() => _AppState();
 | |
| }
 | |
| 
 | |
| class _AppState extends State<App> {
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     WidgetsBinding.instance.window.onPlatformBrightnessChanged = () {
 | |
|       final userPreference = MyTheme.getThemeModePreference();
 | |
|       if (userPreference != ThemeMode.system) return;
 | |
|       WidgetsBinding.instance.handlePlatformBrightnessChanged();
 | |
|       final systemIsDark =
 | |
|           WidgetsBinding.instance.platformDispatcher.platformBrightness ==
 | |
|               Brightness.dark;
 | |
|       final ThemeMode to;
 | |
|       if (systemIsDark) {
 | |
|         to = ThemeMode.dark;
 | |
|       } else {
 | |
|         to = ThemeMode.light;
 | |
|       }
 | |
|       Get.changeThemeMode(to);
 | |
|       // Synchronize the window theme of the system.
 | |
|       updateSystemWindowTheme();
 | |
|       if (desktopType == DesktopType.main) {
 | |
|         bind.mainChangeTheme(dark: to.toShortString());
 | |
|       }
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     // final analytics = FirebaseAnalytics.instance;
 | |
|     final botToastBuilder = BotToastInit();
 | |
|     return RefreshWrapper(builder: (context) {
 | |
|       return MultiProvider(
 | |
|         providers: [
 | |
|           // global configuration
 | |
|           // use session related FFI when in remote control or file transfer page
 | |
|           ChangeNotifierProvider.value(value: gFFI.ffiModel),
 | |
|           ChangeNotifierProvider.value(value: gFFI.imageModel),
 | |
|           ChangeNotifierProvider.value(value: gFFI.cursorModel),
 | |
|           ChangeNotifierProvider.value(value: gFFI.canvasModel),
 | |
|           ChangeNotifierProvider.value(value: gFFI.peerTabModel),
 | |
|         ],
 | |
|         child: GetMaterialApp(
 | |
|           navigatorKey: globalKey,
 | |
|           debugShowCheckedModeBanner: false,
 | |
|           title: 'RustDesk',
 | |
|           theme: MyTheme.lightTheme,
 | |
|           darkTheme: MyTheme.darkTheme,
 | |
|           themeMode: MyTheme.currentThemeMode(),
 | |
|           home: isDesktop
 | |
|               ? const DesktopTabPage()
 | |
|               : isWeb
 | |
|                   ? WebHomePage()
 | |
|                   : HomePage(),
 | |
|           localizationsDelegates: const [
 | |
|             GlobalMaterialLocalizations.delegate,
 | |
|             GlobalWidgetsLocalizations.delegate,
 | |
|             GlobalCupertinoLocalizations.delegate,
 | |
|           ],
 | |
|           supportedLocales: supportedLocales,
 | |
|           navigatorObservers: [
 | |
|             // FirebaseAnalyticsObserver(analytics: analytics),
 | |
|             BotToastNavigatorObserver(),
 | |
|           ],
 | |
|           builder: isAndroid
 | |
|               ? (context, child) => AccessibilityListener(
 | |
|                     child: MediaQuery(
 | |
|                       data: MediaQuery.of(context).copyWith(
 | |
|                         textScaler: TextScaler.linear(1.0),
 | |
|                       ),
 | |
|                       child: child ?? Container(),
 | |
|                     ),
 | |
|                   )
 | |
|               : (context, child) {
 | |
|                   child = _keepScaleBuilder(context, child);
 | |
|                   child = botToastBuilder(context, child);
 | |
|                   if (isDesktop && desktopType == DesktopType.main) {
 | |
|                     child = keyListenerBuilder(context, child);
 | |
|                   }
 | |
|                   if (isLinux) {
 | |
|                     child = buildVirtualWindowFrame(context, child);
 | |
|                   }
 | |
|                   return child;
 | |
|                 },
 | |
|         ),
 | |
|       );
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| Widget _keepScaleBuilder(BuildContext context, Widget? child) {
 | |
|   return MediaQuery(
 | |
|     data: MediaQuery.of(context).copyWith(
 | |
|       textScaler: TextScaler.linear(1.0),
 | |
|     ),
 | |
|     child: child ?? Container(),
 | |
|   );
 | |
| }
 | |
| 
 | |
| _registerEventHandler() {
 | |
|   if (isDesktop && desktopType != DesktopType.main) {
 | |
|     platformFFI.registerEventHandler('theme', 'theme', (evt) async {
 | |
|       String? dark = evt['dark'];
 | |
|       if (dark != null) {
 | |
|         MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
 | |
|       }
 | |
|     });
 | |
|     platformFFI.registerEventHandler('language', 'language', (_) async {
 | |
|       reloadAllWindows();
 | |
|     });
 | |
|   }
 | |
|   // Register native handlers.
 | |
|   if (isDesktop) {
 | |
|     platformFFI.registerEventHandler('native_ui', 'native_ui', (evt) async {
 | |
|       NativeUiHandler.instance.onEvent(evt);
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| Widget keyListenerBuilder(BuildContext context, Widget? child) {
 | |
|   return RawKeyboardListener(
 | |
|     focusNode: FocusNode(),
 | |
|     child: child ?? Container(),
 | |
|     onKey: (RawKeyEvent event) {
 | |
|       if (event.logicalKey == LogicalKeyboardKey.shiftLeft) {
 | |
|         if (event is RawKeyDownEvent) {
 | |
|           gFFI.peerTabModel.setShiftDown(true);
 | |
|         } else if (event is RawKeyUpEvent) {
 | |
|           gFFI.peerTabModel.setShiftDown(false);
 | |
|         }
 | |
|       }
 | |
|     },
 | |
|   );
 | |
| }
 |