123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- 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 {
- earlyAssert();
- 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());
- 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.mainHideDock();
- 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.mainHideDock();
- await windowManager.minimize();
- await windowManager.hide();
- _isCmReadyToShow = true;
- } else if (_isCmReadyToShow) {
- if (await windowManager.getOpacity() != 0) {
- await windowManager.setOpacity(0);
- bind.mainHideDock();
- 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> with WidgetsBindingObserver {
- @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());
- }
- };
- WidgetsBinding.instance.addObserver(this);
- WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation());
- }
- @override
- void dispose() {
- WidgetsBinding.instance.removeObserver(this);
- super.dispose();
- }
- @override
- void didChangeMetrics() {
- _updateOrientation();
- }
- void _updateOrientation() {
- if (isDesktop) return;
- // Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`,
- // my test (Flutter 3.19.6, Android 14) is always the reverse value.
- // https://github.com/flutter/flutter/issues/60899
- // stateGlobal.isPortrait.value =
- // MediaQuery.of(context).orientation == Orientation.portrait;
- final orientation = View.of(context).physicalSize.aspectRatio > 1
- ? Orientation.landscape
- : Orientation.portrait;
- stateGlobal.isPortrait.value = orientation == Orientation.portrait;
- }
- @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: isWeb
- ? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)'
- : bind.mainGetAppNameSync(),
- 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) ||
- isWebDesktop) {
- 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) {
- await 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);
- }
- }
- },
- );
- }
|