rustdesk/flutter/lib/main.dart
21pages 1212d9fa2d
web uni link (#9697)
Signed-off-by: 21pages <sunboeasy@gmail.com>
2024-10-19 15:32:17 +08:00

540 lines
17 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 {
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);
}
}
},
);
}