main.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'package:bot_toast/bot_toast.dart';
  5. import 'package:desktop_multi_window/desktop_multi_window.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/services.dart';
  8. import 'package:flutter_hbb/common/widgets/overlay.dart';
  9. import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
  10. import 'package:flutter_hbb/desktop/pages/install_page.dart';
  11. import 'package:flutter_hbb/desktop/pages/server_page.dart';
  12. import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart';
  13. import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart';
  14. import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart';
  15. import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart';
  16. import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
  17. import 'package:flutter_hbb/models/state_model.dart';
  18. import 'package:flutter_hbb/utils/multi_window_manager.dart';
  19. import 'package:flutter_localizations/flutter_localizations.dart';
  20. import 'package:get/get.dart';
  21. import 'package:provider/provider.dart';
  22. import 'package:window_manager/window_manager.dart';
  23. import 'common.dart';
  24. import 'consts.dart';
  25. import 'mobile/pages/home_page.dart';
  26. import 'mobile/pages/server_page.dart';
  27. import 'models/platform_model.dart';
  28. import 'package:flutter_hbb/plugin/handlers.dart'
  29. if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
  30. /// Basic window and launch properties.
  31. int? kWindowId;
  32. WindowType? kWindowType;
  33. late List<String> kBootArgs;
  34. Future<void> main(List<String> args) async {
  35. earlyAssert();
  36. WidgetsFlutterBinding.ensureInitialized();
  37. debugPrint("launch args: $args");
  38. kBootArgs = List.from(args);
  39. if (!isDesktop) {
  40. runMobileApp();
  41. return;
  42. }
  43. // main window
  44. if (args.isNotEmpty && args.first == 'multi_window') {
  45. kWindowId = int.parse(args[1]);
  46. stateGlobal.setWindowId(kWindowId!);
  47. if (!isMacOS) {
  48. WindowController.fromWindowId(kWindowId!).showTitleBar(false);
  49. }
  50. final argument = args[2].isEmpty
  51. ? <String, dynamic>{}
  52. : jsonDecode(args[2]) as Map<String, dynamic>;
  53. int type = argument['type'] ?? -1;
  54. // to-do: No need to parse window id ?
  55. // Because stateGlobal.windowId is a global value.
  56. argument['windowId'] = kWindowId;
  57. kWindowType = type.windowType;
  58. switch (kWindowType) {
  59. case WindowType.RemoteDesktop:
  60. desktopType = DesktopType.remote;
  61. runMultiWindow(
  62. argument,
  63. kAppTypeDesktopRemote,
  64. );
  65. break;
  66. case WindowType.FileTransfer:
  67. desktopType = DesktopType.fileTransfer;
  68. runMultiWindow(
  69. argument,
  70. kAppTypeDesktopFileTransfer,
  71. );
  72. break;
  73. case WindowType.ViewCamera:
  74. desktopType = DesktopType.viewCamera;
  75. runMultiWindow(
  76. argument,
  77. kAppTypeDesktopViewCamera,
  78. );
  79. break;
  80. case WindowType.PortForward:
  81. desktopType = DesktopType.portForward;
  82. runMultiWindow(
  83. argument,
  84. kAppTypeDesktopPortForward,
  85. );
  86. break;
  87. default:
  88. break;
  89. }
  90. } else if (args.isNotEmpty && args.first == '--cm') {
  91. debugPrint("--cm started");
  92. desktopType = DesktopType.cm;
  93. await windowManager.ensureInitialized();
  94. runConnectionManagerScreen();
  95. } else if (args.contains('--install')) {
  96. runInstallPage();
  97. } else {
  98. desktopType = DesktopType.main;
  99. await windowManager.ensureInitialized();
  100. windowManager.setPreventClose(true);
  101. if (isMacOS) {
  102. disableWindowMovable(kWindowId);
  103. }
  104. runMainApp(true);
  105. }
  106. }
  107. Future<void> initEnv(String appType) async {
  108. // global shared preference
  109. await platformFFI.init(appType);
  110. // global FFI, use this **ONLY** for global configuration
  111. // for convenience, use global FFI on mobile platform
  112. // focus on multi-ffi on desktop first
  113. await initGlobalFFI();
  114. // await Firebase.initializeApp();
  115. _registerEventHandler();
  116. // Update the system theme.
  117. updateSystemWindowTheme();
  118. }
  119. void runMainApp(bool startService) async {
  120. // register uni links
  121. await initEnv(kAppTypeMain);
  122. checkUpdate();
  123. // trigger connection status updater
  124. await bind.mainCheckConnectStatus();
  125. if (startService) {
  126. gFFI.serverModel.startService();
  127. bind.pluginSyncUi(syncTo: kAppTypeMain);
  128. bind.pluginListReload();
  129. }
  130. await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
  131. gFFI.userModel.refreshCurrentUser();
  132. runApp(App());
  133. // Set window option.
  134. WindowOptions windowOptions =
  135. getHiddenTitleBarWindowOptions(isMainWindow: true);
  136. windowManager.waitUntilReadyToShow(windowOptions, () async {
  137. // Restore the location of the main window before window hide or show.
  138. await restoreWindowPosition(WindowType.Main);
  139. // Check the startup argument, if we successfully handle the argument, we keep the main window hidden.
  140. final handledByUniLinks = await initUniLinks();
  141. debugPrint("handled by uni links: $handledByUniLinks");
  142. if (handledByUniLinks || handleUriLink(cmdArgs: kBootArgs)) {
  143. windowManager.hide();
  144. } else {
  145. windowManager.show();
  146. windowManager.focus();
  147. // Move registration of active main window here to prevent from async visible check.
  148. rustDeskWinManager.registerActiveWindow(kWindowMainId);
  149. }
  150. windowManager.setOpacity(1);
  151. windowManager.setTitle(getWindowName());
  152. // Do not use `windowManager.setResizable()` here.
  153. setResizable(!bind.isIncomingOnly());
  154. });
  155. }
  156. void runMobileApp() async {
  157. await initEnv(kAppTypeMain);
  158. checkUpdate();
  159. if (isAndroid) androidChannelInit();
  160. if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath();
  161. draggablePositions.load();
  162. await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]);
  163. gFFI.userModel.refreshCurrentUser();
  164. runApp(App());
  165. await initUniLinks();
  166. }
  167. void runMultiWindow(
  168. Map<String, dynamic> argument,
  169. String appType,
  170. ) async {
  171. await initEnv(appType);
  172. final title = getWindowName();
  173. // set prevent close to true, we handle close event manually
  174. WindowController.fromWindowId(kWindowId!).setPreventClose(true);
  175. if (isMacOS) {
  176. disableWindowMovable(kWindowId);
  177. }
  178. late Widget widget;
  179. switch (appType) {
  180. case kAppTypeDesktopRemote:
  181. draggablePositions.load();
  182. widget = DesktopRemoteScreen(
  183. params: argument,
  184. );
  185. break;
  186. case kAppTypeDesktopFileTransfer:
  187. widget = DesktopFileTransferScreen(
  188. params: argument,
  189. );
  190. break;
  191. case kAppTypeDesktopViewCamera:
  192. draggablePositions.load();
  193. widget = DesktopViewCameraScreen(
  194. params: argument,
  195. );
  196. break;
  197. case kAppTypeDesktopPortForward:
  198. widget = DesktopPortForwardScreen(
  199. params: argument,
  200. );
  201. break;
  202. default:
  203. // no such appType
  204. exit(0);
  205. }
  206. _runApp(
  207. title,
  208. widget,
  209. MyTheme.currentThemeMode(),
  210. );
  211. // we do not hide titlebar on win7 because of the frame overflow.
  212. if (kUseCompatibleUiMode) {
  213. WindowController.fromWindowId(kWindowId!).showTitleBar(true);
  214. }
  215. switch (appType) {
  216. case kAppTypeDesktopRemote:
  217. // If screen rect is set, the window will be moved to the target screen and then set fullscreen.
  218. if (argument['screen_rect'] == null) {
  219. // display can be used to control the offset of the window.
  220. await restoreWindowPosition(
  221. WindowType.RemoteDesktop,
  222. windowId: kWindowId!,
  223. peerId: argument['id'] as String?,
  224. display: argument['display'] as int?,
  225. );
  226. }
  227. break;
  228. case kAppTypeDesktopFileTransfer:
  229. await restoreWindowPosition(WindowType.FileTransfer,
  230. windowId: kWindowId!);
  231. break;
  232. case kAppTypeDesktopViewCamera:
  233. // If screen rect is set, the window will be moved to the target screen and then set fullscreen.
  234. if (argument['screen_rect'] == null) {
  235. // display can be used to control the offset of the window.
  236. await restoreWindowPosition(
  237. WindowType.ViewCamera,
  238. windowId: kWindowId!,
  239. peerId: argument['id'] as String?,
  240. // FIXME: fix display index.
  241. display: argument['display'] as int?,
  242. );
  243. }
  244. break;
  245. case kAppTypeDesktopPortForward:
  246. await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!);
  247. break;
  248. default:
  249. // no such appType
  250. exit(0);
  251. }
  252. // show window from hidden status
  253. WindowController.fromWindowId(kWindowId!).show();
  254. }
  255. void runConnectionManagerScreen() async {
  256. await initEnv(kAppTypeConnectionManager);
  257. _runApp(
  258. '',
  259. const DesktopServerPage(),
  260. MyTheme.currentThemeMode(),
  261. );
  262. final hide = await bind.cmGetConfig(name: "hide_cm") == 'true';
  263. gFFI.serverModel.hideCm = hide;
  264. if (hide) {
  265. await hideCmWindow(isStartup: true);
  266. } else {
  267. await showCmWindow(isStartup: true);
  268. }
  269. setResizable(false);
  270. // Start the uni links handler and redirect links to Native, not for Flutter.
  271. listenUniLinks(handleByFlutter: false);
  272. }
  273. bool _isCmReadyToShow = false;
  274. showCmWindow({bool isStartup = false}) async {
  275. if (isStartup) {
  276. WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
  277. size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
  278. await windowManager.waitUntilReadyToShow(windowOptions, null);
  279. bind.mainHideDock();
  280. await Future.wait([
  281. windowManager.show(),
  282. windowManager.focus(),
  283. windowManager.setOpacity(1)
  284. ]);
  285. // ensure initial window size to be changed
  286. await windowManager.setSizeAlignment(
  287. kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
  288. _isCmReadyToShow = true;
  289. } else if (_isCmReadyToShow) {
  290. if (await windowManager.getOpacity() != 1) {
  291. await windowManager.setOpacity(1);
  292. await windowManager.focus();
  293. await windowManager.minimize(); //needed
  294. await windowManager.setSizeAlignment(
  295. kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
  296. windowOnTop(null);
  297. }
  298. }
  299. }
  300. hideCmWindow({bool isStartup = false}) async {
  301. if (isStartup) {
  302. WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
  303. size: kConnectionManagerWindowSizeClosedChat);
  304. windowManager.setOpacity(0);
  305. await windowManager.waitUntilReadyToShow(windowOptions, null);
  306. bind.mainHideDock();
  307. await windowManager.minimize();
  308. await windowManager.hide();
  309. _isCmReadyToShow = true;
  310. } else if (_isCmReadyToShow) {
  311. if (await windowManager.getOpacity() != 0) {
  312. await windowManager.setOpacity(0);
  313. bind.mainHideDock();
  314. await windowManager.minimize();
  315. await windowManager.hide();
  316. }
  317. }
  318. }
  319. void _runApp(
  320. String title,
  321. Widget home,
  322. ThemeMode themeMode,
  323. ) {
  324. final botToastBuilder = BotToastInit();
  325. runApp(RefreshWrapper(
  326. builder: (context) => GetMaterialApp(
  327. navigatorKey: globalKey,
  328. debugShowCheckedModeBanner: false,
  329. title: title,
  330. theme: MyTheme.lightTheme,
  331. darkTheme: MyTheme.darkTheme,
  332. themeMode: themeMode,
  333. home: home,
  334. localizationsDelegates: const [
  335. GlobalMaterialLocalizations.delegate,
  336. GlobalWidgetsLocalizations.delegate,
  337. GlobalCupertinoLocalizations.delegate,
  338. ],
  339. supportedLocales: supportedLocales,
  340. navigatorObservers: [
  341. // FirebaseAnalyticsObserver(analytics: analytics),
  342. BotToastNavigatorObserver(),
  343. ],
  344. builder: (context, child) {
  345. child = _keepScaleBuilder(context, child);
  346. child = botToastBuilder(context, child);
  347. return child;
  348. },
  349. ),
  350. ));
  351. }
  352. void runInstallPage() async {
  353. await windowManager.ensureInitialized();
  354. await initEnv(kAppTypeMain);
  355. _runApp('', const InstallPage(), MyTheme.currentThemeMode());
  356. WindowOptions windowOptions =
  357. getHiddenTitleBarWindowOptions(size: Size(800, 600), center: true);
  358. windowManager.waitUntilReadyToShow(windowOptions, () async {
  359. windowManager.show();
  360. windowManager.focus();
  361. windowManager.setOpacity(1);
  362. windowManager.setAlignment(Alignment.center); // ensure
  363. });
  364. }
  365. WindowOptions getHiddenTitleBarWindowOptions(
  366. {bool isMainWindow = false,
  367. Size? size,
  368. bool center = false,
  369. bool? alwaysOnTop}) {
  370. var defaultTitleBarStyle = TitleBarStyle.hidden;
  371. // we do not hide titlebar on win7 because of the frame overflow.
  372. if (kUseCompatibleUiMode) {
  373. defaultTitleBarStyle = TitleBarStyle.normal;
  374. }
  375. return WindowOptions(
  376. size: size,
  377. center: center,
  378. backgroundColor: (isMacOS && isMainWindow) ? null : Colors.transparent,
  379. skipTaskbar: false,
  380. titleBarStyle: defaultTitleBarStyle,
  381. alwaysOnTop: alwaysOnTop,
  382. );
  383. }
  384. class App extends StatefulWidget {
  385. @override
  386. State<App> createState() => _AppState();
  387. }
  388. class _AppState extends State<App> with WidgetsBindingObserver {
  389. @override
  390. void initState() {
  391. super.initState();
  392. WidgetsBinding.instance.window.onPlatformBrightnessChanged = () {
  393. final userPreference = MyTheme.getThemeModePreference();
  394. if (userPreference != ThemeMode.system) return;
  395. WidgetsBinding.instance.handlePlatformBrightnessChanged();
  396. final systemIsDark =
  397. WidgetsBinding.instance.platformDispatcher.platformBrightness ==
  398. Brightness.dark;
  399. final ThemeMode to;
  400. if (systemIsDark) {
  401. to = ThemeMode.dark;
  402. } else {
  403. to = ThemeMode.light;
  404. }
  405. Get.changeThemeMode(to);
  406. // Synchronize the window theme of the system.
  407. updateSystemWindowTheme();
  408. if (desktopType == DesktopType.main) {
  409. bind.mainChangeTheme(dark: to.toShortString());
  410. }
  411. };
  412. WidgetsBinding.instance.addObserver(this);
  413. WidgetsBinding.instance.addPostFrameCallback((_) => _updateOrientation());
  414. }
  415. @override
  416. void dispose() {
  417. WidgetsBinding.instance.removeObserver(this);
  418. super.dispose();
  419. }
  420. @override
  421. void didChangeMetrics() {
  422. _updateOrientation();
  423. }
  424. void _updateOrientation() {
  425. if (isDesktop) return;
  426. // Don't use `MediaQuery.of(context).orientation` in `didChangeMetrics()`,
  427. // my test (Flutter 3.19.6, Android 14) is always the reverse value.
  428. // https://github.com/flutter/flutter/issues/60899
  429. // stateGlobal.isPortrait.value =
  430. // MediaQuery.of(context).orientation == Orientation.portrait;
  431. final orientation = View.of(context).physicalSize.aspectRatio > 1
  432. ? Orientation.landscape
  433. : Orientation.portrait;
  434. stateGlobal.isPortrait.value = orientation == Orientation.portrait;
  435. }
  436. @override
  437. Widget build(BuildContext context) {
  438. // final analytics = FirebaseAnalytics.instance;
  439. final botToastBuilder = BotToastInit();
  440. return RefreshWrapper(builder: (context) {
  441. return MultiProvider(
  442. providers: [
  443. // global configuration
  444. // use session related FFI when in remote control or file transfer page
  445. ChangeNotifierProvider.value(value: gFFI.ffiModel),
  446. ChangeNotifierProvider.value(value: gFFI.imageModel),
  447. ChangeNotifierProvider.value(value: gFFI.cursorModel),
  448. ChangeNotifierProvider.value(value: gFFI.canvasModel),
  449. ChangeNotifierProvider.value(value: gFFI.peerTabModel),
  450. ],
  451. child: GetMaterialApp(
  452. navigatorKey: globalKey,
  453. debugShowCheckedModeBanner: false,
  454. title: isWeb
  455. ? '${bind.mainGetAppNameSync()} Web Client V2 (Preview)'
  456. : bind.mainGetAppNameSync(),
  457. theme: MyTheme.lightTheme,
  458. darkTheme: MyTheme.darkTheme,
  459. themeMode: MyTheme.currentThemeMode(),
  460. home: isDesktop
  461. ? const DesktopTabPage()
  462. : isWeb
  463. ? WebHomePage()
  464. : HomePage(),
  465. localizationsDelegates: const [
  466. GlobalMaterialLocalizations.delegate,
  467. GlobalWidgetsLocalizations.delegate,
  468. GlobalCupertinoLocalizations.delegate,
  469. ],
  470. supportedLocales: supportedLocales,
  471. navigatorObservers: [
  472. // FirebaseAnalyticsObserver(analytics: analytics),
  473. BotToastNavigatorObserver(),
  474. ],
  475. builder: isAndroid
  476. ? (context, child) => AccessibilityListener(
  477. child: MediaQuery(
  478. data: MediaQuery.of(context).copyWith(
  479. textScaler: TextScaler.linear(1.0),
  480. ),
  481. child: child ?? Container(),
  482. ),
  483. )
  484. : (context, child) {
  485. child = _keepScaleBuilder(context, child);
  486. child = botToastBuilder(context, child);
  487. if ((isDesktop && desktopType == DesktopType.main) ||
  488. isWebDesktop) {
  489. child = keyListenerBuilder(context, child);
  490. }
  491. if (isLinux) {
  492. return buildVirtualWindowFrame(context, child);
  493. } else {
  494. return workaroundWindowBorder(context, child);
  495. }
  496. },
  497. ),
  498. );
  499. });
  500. }
  501. }
  502. Widget _keepScaleBuilder(BuildContext context, Widget? child) {
  503. return MediaQuery(
  504. data: MediaQuery.of(context).copyWith(
  505. textScaler: TextScaler.linear(1.0),
  506. ),
  507. child: child ?? Container(),
  508. );
  509. }
  510. _registerEventHandler() {
  511. if (isDesktop && desktopType != DesktopType.main) {
  512. platformFFI.registerEventHandler('theme', 'theme', (evt) async {
  513. String? dark = evt['dark'];
  514. if (dark != null) {
  515. await MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark));
  516. }
  517. });
  518. platformFFI.registerEventHandler('language', 'language', (_) async {
  519. reloadAllWindows();
  520. });
  521. }
  522. // Register native handlers.
  523. if (isDesktop) {
  524. platformFFI.registerEventHandler('native_ui', 'native_ui', (evt) async {
  525. NativeUiHandler.instance.onEvent(evt);
  526. });
  527. }
  528. }
  529. Widget keyListenerBuilder(BuildContext context, Widget? child) {
  530. return RawKeyboardListener(
  531. focusNode: FocusNode(),
  532. child: child ?? Container(),
  533. onKey: (RawKeyEvent event) {
  534. if (event.logicalKey == LogicalKeyboardKey.shiftLeft) {
  535. if (event is RawKeyDownEvent) {
  536. gFFI.peerTabModel.setShiftDown(true);
  537. } else if (event is RawKeyUpEvent) {
  538. gFFI.peerTabModel.setShiftDown(false);
  539. }
  540. }
  541. },
  542. );
  543. }