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 kBootArgs; Future main(List 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 ? {} : jsonDecode(args[2]) as Map; 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 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 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 createState() => _AppState(); } class _AppState extends State { @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); } } }, ); }