From a3a893d7aae307691ddda678327033bef01c16d5 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Thu, 28 Dec 2023 23:16:56 +0330 Subject: [PATCH] Refactor desktop window management and tray --- lib/bootstrap.dart | 51 ++++++------ lib/features/app/widget/app.dart | 73 ++++++++++------- .../notifier/auto_start_notifier.dart} | 6 +- lib/features/common/common_controllers.dart | 41 ---------- .../common/window/window_controller.dart | 80 ------------------- .../connection/widget/connection_wrapper.dart | 39 +++++++++ .../widgets/general_setting_tiles.dart | 8 +- .../system_tray_notifier.dart} | 78 ++++++------------ .../widget/system_tray_wrapper.dart | 53 ++++++++++++ .../window/notifier/window_notifier.dart | 54 +++++++++++++ .../window/widget/window_wrapper.dart | 54 +++++++++++++ .../wrapper/shortcut/shortcut_wrapper.dart | 6 +- linux/my_application.cc | 2 +- macos/Runner/AppDelegate.swift | 26 +++--- macos/Runner/MainFlutterWindow.swift | 9 ++- windows/runner/flutter_window.cpp | 3 +- windows/runner/win32_window.cpp | 4 +- 17 files changed, 334 insertions(+), 253 deletions(-) rename lib/{services/auto_start_service.dart => features/auto_start/notifier/auto_start_notifier.dart} (88%) delete mode 100644 lib/features/common/common_controllers.dart delete mode 100644 lib/features/common/window/window_controller.dart create mode 100644 lib/features/connection/widget/connection_wrapper.dart rename lib/features/system_tray/{system_tray_controller.dart => notifier/system_tray_notifier.dart} (67%) create mode 100644 lib/features/system_tray/widget/system_tray_wrapper.dart create mode 100644 lib/features/window/notifier/window_notifier.dart create mode 100644 lib/features/window/widget/window_wrapper.dart diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index cacf050c..99eff26f 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -14,20 +14,19 @@ import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/preferences/preferences_migration.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/features/app/widget/app.dart'; -import 'package:hiddify/features/common/window/window_controller.dart'; +import 'package:hiddify/features/auto_start/notifier/auto_start_notifier.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; import 'package:hiddify/features/log/data/log_data_providers.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; -import 'package:hiddify/features/system_tray/system_tray_controller.dart'; -import 'package:hiddify/services/auto_start_service.dart'; +import 'package:hiddify/features/system_tray/notifier/system_tray_notifier.dart'; +import 'package:hiddify/features/window/notifier/window_notifier.dart'; import 'package:hiddify/services/deep_link_service.dart'; import 'package:hiddify/singbox/service/singbox_service_provider.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:loggy/loggy.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:window_manager/window_manager.dart'; Future lazyBootstrap( WidgetsBinding widgetsBinding, @@ -42,7 +41,6 @@ Future lazyBootstrap( Logger.logPlatformDispatcherError; final stopWatch = Stopwatch()..start(); - if (PlatformUtils.isDesktop) await windowManager.ensureInitialized(); final container = ProviderContainer( overrides: [ @@ -95,6 +93,27 @@ Future lazyBootstrap( final debug = container.read(debugModeNotifierProvider) || kDebugMode; + if (PlatformUtils.isDesktop) { + await _init( + "window controller", + () => container.read(windowNotifierProvider.future), + ); + + final silentStart = container.read(silentStartNotifierProvider); + Logger.bootstrap + .debug("silent start [${silentStart ? "Enabled" : "Disabled"}]"); + if (!silentStart) { + await container.read(windowNotifierProvider.notifier).open(focus: false); + } else { + Logger.bootstrap.debug("silent start, remain hidden accessible via tray"); + } + + await _init( + "auto start service", + () => container.read(autoStartNotifierProvider.future), + ); + } + await _init( "logs repository", () => container.read(logRepositoryProvider.future), @@ -111,24 +130,6 @@ Future lazyBootstrap( () => container.read(profileRepositoryProvider.future), ); - final silentStart = container.read(silentStartNotifierProvider); - Logger.bootstrap - .debug("silent start [${silentStart ? "Enabled" : "Disabled"}]"); - if (silentStart) { - FlutterNativeSplash.remove(); - } - - if (PlatformUtils.isDesktop) { - await _init( - "auto start service", - () => container.read(autoStartServiceProvider.future), - ); - await _init( - "window controller", - () => container.read(windowControllerProvider.future), - ); - } - await _init( "sing-box", () => container.read(singboxServiceProvider).init(), @@ -146,7 +147,7 @@ Future lazyBootstrap( if (PlatformUtils.isDesktop) { await _safeInit( "system tray", - () => container.read(systemTrayControllerProvider.future), + () => container.read(systemTrayNotifierProvider.future), timeout: 1000, ); } @@ -163,7 +164,7 @@ Future lazyBootstrap( ), ); - if (!silentStart) FlutterNativeSplash.remove(); + FlutterNativeSplash.remove(); } Future _init( diff --git a/lib/features/app/widget/app.dart b/lib/features/app/widget/app.dart index 3841232b..e97fd720 100644 --- a/lib/features/app/widget/app.dart +++ b/lib/features/app/widget/app.dart @@ -6,11 +6,15 @@ import 'package:hiddify/core/localization/locale_extensions.dart'; import 'package:hiddify/core/localization/locale_preferences.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/constants.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/core/theme/app_theme.dart'; import 'package:hiddify/core/theme/theme_preferences.dart'; import 'package:hiddify/features/app_update/notifier/app_update_notifier.dart'; -import 'package:hiddify/features/common/common_controllers.dart'; +import 'package:hiddify/features/connection/widget/connection_wrapper.dart'; +import 'package:hiddify/features/profile/notifier/profiles_update_notifier.dart'; +import 'package:hiddify/features/system_tray/widget/system_tray_wrapper.dart'; +import 'package:hiddify/features/window/widget/window_wrapper.dart'; import 'package:hiddify/features/wrapper/shortcut/shortcut_wrapper.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -28,35 +32,48 @@ class App extends HookConsumerWidget with PresLogger { final themeMode = ref.watch(themePreferencesProvider); final theme = AppTheme(themeMode, locale.preferredFontFamily); - ref.watch(commonControllersProvider); - final upgrader = ref.watch(upgraderProvider); - return ShortcutWrapper( - MaterialApp.router( - routerConfig: router, - locale: locale.flutterLocale, - supportedLocales: AppLocaleUtils.supportedLocales, - localizationsDelegates: GlobalMaterialLocalizations.delegates, - debugShowCheckedModeBanner: false, - themeMode: themeMode.flutterThemeMode, - theme: theme.light(), - darkTheme: theme.dark(), - title: Constants.appName, - builder: (context, child) { - child = UpgradeAlert( - upgrader: upgrader, - navigatorKey: router.routerDelegate.navigatorKey, - child: child ?? const SizedBox(), - ); - if (kDebugMode && _debugAccessibility) { - return AccessibilityTools( - checkFontOverflows: true, - child: child, - ); - } - return child; - }, + ref.listen( + introCompletedProvider, + (_, completed) async { + if (completed) { + await ref.read(foregroundProfilesUpdateNotifierProvider.future); + } + }, + ); + + return WindowWrapper( + TrayWrapper( + ShortcutWrapper( + ConnectionWrapper( + MaterialApp.router( + routerConfig: router, + locale: locale.flutterLocale, + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: GlobalMaterialLocalizations.delegates, + debugShowCheckedModeBanner: false, + themeMode: themeMode.flutterThemeMode, + theme: theme.light(), + darkTheme: theme.dark(), + title: Constants.appName, + builder: (context, child) { + child = UpgradeAlert( + upgrader: upgrader, + navigatorKey: router.routerDelegate.navigatorKey, + child: child ?? const SizedBox(), + ); + if (kDebugMode && _debugAccessibility) { + return AccessibilityTools( + checkFontOverflows: true, + child: child, + ); + } + return child; + }, + ), + ), + ), ), ); } diff --git a/lib/services/auto_start_service.dart b/lib/features/auto_start/notifier/auto_start_notifier.dart similarity index 88% rename from lib/services/auto_start_service.dart rename to lib/features/auto_start/notifier/auto_start_notifier.dart index 7c60739e..a30315b0 100644 --- a/lib/services/auto_start_service.dart +++ b/lib/features/auto_start/notifier/auto_start_notifier.dart @@ -5,14 +5,14 @@ import 'package:hiddify/utils/utils.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'auto_start_service.g.dart'; +part 'auto_start_notifier.g.dart'; @Riverpod(keepAlive: true) -class AutoStartService extends _$AutoStartService with InfraLogger { +class AutoStartNotifier extends _$AutoStartNotifier with InfraLogger { @override Future build() async { - loggy.debug("initializing"); if (!PlatformUtils.isDesktop) return false; + final appInfo = ref.watch(appInfoProvider).requireValue; launchAtStartup.setup( appName: appInfo.name, diff --git a/lib/features/common/common_controllers.dart b/lib/features/common/common_controllers.dart deleted file mode 100644 index b0e63882..00000000 --- a/lib/features/common/common_controllers.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:hiddify/core/preferences/general_preferences.dart'; -import 'package:hiddify/features/common/window/window_controller.dart'; -import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; -import 'package:hiddify/features/profile/notifier/profiles_update_notifier.dart'; -import 'package:hiddify/features/system_tray/system_tray_controller.dart'; -import 'package:hiddify/utils/platform_utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'common_controllers.g.dart'; - -// this is a temporary solution to keep providers running even when there are no active listeners -// https://github.com/rrousselGit/riverpod/discussions/2730 -@Riverpod(keepAlive: true) -void commonControllers(CommonControllersRef ref) { - ref.listen( - introCompletedProvider, - (_, completed) async { - if (completed) { - await ref.read(foregroundProfilesUpdateNotifierProvider.future); - } - }, - fireImmediately: true, - ); - ref.listen( - connectionNotifierProvider, - (previous, next) {}, - fireImmediately: true, - ); - if (PlatformUtils.isDesktop) { - ref.listen( - windowControllerProvider, - (previous, next) {}, - fireImmediately: true, - ); - ref.listen( - systemTrayControllerProvider, - (previous, next) {}, - fireImmediately: true, - ); - } -} diff --git a/lib/features/common/window/window_controller.dart b/lib/features/common/window/window_controller.dart deleted file mode 100644 index b75ad15a..00000000 --- a/lib/features/common/window/window_controller.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hiddify/core/app_info/app_info_provider.dart'; -import 'package:hiddify/core/model/constants.dart'; -import 'package:hiddify/core/preferences/general_preferences.dart'; -import 'package:hiddify/core/preferences/service_preferences.dart'; -import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:window_manager/window_manager.dart'; - -part 'window_controller.g.dart'; - -// TODO improve -@Riverpod(keepAlive: true) -class WindowController extends _$WindowController - with WindowListener, AppLogger { - @override - Future build() async { - await windowManager.ensureInitialized(); - const size = Size(868, 668); - const minimumSize = Size(368, 568); - const windowOptions = WindowOptions( - size: size, - minimumSize: minimumSize, - center: true, - ); - await windowManager.setPreventClose(true); - await windowManager.waitUntilReadyToShow( - windowOptions, - () async { - final version = await ref.watch(appInfoProvider.future); - await windowManager - .setTitle("${Constants.appName} ${version.presentVersion}"); - - if (ref.read(silentStartNotifierProvider)) { - loggy.debug("silent start is enabled, hiding window"); - await windowManager.hide(); - } - await Future.delayed( - const Duration(seconds: 3), - () async { - if (ref.read(startedByUserProvider)) { - loggy.debug("previously started by user, trying to connect"); - return ref.read(connectionNotifierProvider.notifier).mayConnect(); - } - }, - ); - }, - ); - windowManager.addListener(this); - - ref.onDispose(() { - loggy.debug("disposing"); - windowManager.removeListener(this); - }); - return windowManager.isVisible(); - } - - Future show() async { - await windowManager.show(); - await windowManager.focus(); - state = const AsyncData(true); - } - - Future hide() async { - await windowManager.close(); - } - - Future quit() async { - loggy.debug("quitting"); - await windowManager.close(); - await windowManager.destroy(); - } - - @override - Future onWindowClose() async { - await windowManager.hide(); - state = const AsyncData(false); - } -} diff --git a/lib/features/connection/widget/connection_wrapper.dart b/lib/features/connection/widget/connection_wrapper.dart new file mode 100644 index 00000000..1afc73c6 --- /dev/null +++ b/lib/features/connection/widget/connection_wrapper.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/preferences/service_preferences.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/platform_utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class ConnectionWrapper extends StatefulHookConsumerWidget { + const ConnectionWrapper(this.child, {super.key}); + + final Widget child; + + @override + ConsumerState createState() => + _ConnectionWrapperState(); +} + +class _ConnectionWrapperState extends ConsumerState + with AppLogger { + @override + Widget build(BuildContext context) { + ref.listen(connectionNotifierProvider, (_, __) {}); + + return widget.child; + } + + @override + void initState() { + super.initState(); + Future.delayed(const Duration(seconds: 2)).then( + (_) async { + if (ref.read(startedByUserProvider) && PlatformUtils.isDesktop) { + loggy.debug("previously started by user, trying to connect"); + return ref.read(connectionNotifierProvider.notifier).mayConnect(); + } + }, + ); + } +} diff --git a/lib/features/settings/widgets/general_setting_tiles.dart b/lib/features/settings/widgets/general_setting_tiles.dart index 6d80e5a1..282cfe18 100644 --- a/lib/features/settings/widgets/general_setting_tiles.dart +++ b/lib/features/settings/widgets/general_setting_tiles.dart @@ -5,8 +5,8 @@ import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/theme/app_theme_mode.dart'; import 'package:hiddify/core/theme/theme_preferences.dart'; +import 'package:hiddify/features/auto_start/notifier/auto_start_notifier.dart'; import 'package:hiddify/features/common/general_pref_tiles.dart'; -import 'package:hiddify/services/auto_start_service.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -67,12 +67,12 @@ class GeneralSettingTiles extends HookConsumerWidget { if (PlatformUtils.isDesktop) ...[ SwitchListTile( title: Text(t.settings.general.autoStart), - value: ref.watch(autoStartServiceProvider).asData!.value, + value: ref.watch(autoStartNotifierProvider).asData!.value, onChanged: (value) async { if (value) { - await ref.read(autoStartServiceProvider.notifier).enable(); + await ref.read(autoStartNotifierProvider.notifier).enable(); } else { - await ref.read(autoStartServiceProvider.notifier).disable(); + await ref.read(autoStartNotifierProvider.notifier).disable(); } }, ), diff --git a/lib/features/system_tray/system_tray_controller.dart b/lib/features/system_tray/notifier/system_tray_notifier.dart similarity index 67% rename from lib/features/system_tray/system_tray_controller.dart rename to lib/features/system_tray/notifier/system_tray_notifier.dart index f2b1bc59..37a9cbdb 100644 --- a/lib/features/system_tray/system_tray_controller.dart +++ b/lib/features/system_tray/notifier/system_tray_notifier.dart @@ -3,34 +3,30 @@ import 'dart:io'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/features/common/window/window_controller.dart'; import 'package:hiddify/features/config_option/model/config_option_patch.dart'; import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; import 'package:hiddify/features/connection/model/connection_status.dart'; import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; +import 'package:hiddify/features/window/notifier/window_notifier.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/singbox/model/singbox_config_enum.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:tray_manager/tray_manager.dart'; -part 'system_tray_controller.g.dart'; +part 'system_tray_notifier.g.dart'; @Riverpod(keepAlive: true) -class SystemTrayController extends _$SystemTrayController - with TrayListener, AppLogger { +class SystemTrayNotifier extends _$SystemTrayNotifier with AppLogger { @override Future build() async { - if (!_initialized) { - loggy.debug('initializing'); - await trayManager.setIcon( - _trayIconPath, - isTemplate: Platform.isMacOS, - ); - if (!Platform.isLinux) await trayManager.setToolTip(Constants.appName); - trayManager.addListener(this); - _initialized = true; - } + if (!PlatformUtils.isDesktop) return; + + await trayManager.setIcon( + _trayIconPath, + isTemplate: Platform.isMacOS, + ); + if (!Platform.isLinux) await trayManager.setToolTip(Constants.appName); ConnectionStatus connection; try { @@ -55,11 +51,13 @@ class SystemTrayController extends _$SystemTrayController loggy.debug('updating system tray'); - final trayMenu = Menu( + final menu = Menu( items: [ MenuItem( label: t.tray.dashboard, - onClick: handleClickShowApp, + onClick: (_) async { + await ref.read(windowNotifierProvider.notifier).open(); + }, ), MenuItem.separator(), MenuItem.checkbox( @@ -71,7 +69,11 @@ class SystemTrayController extends _$SystemTrayController }, checked: connection.isConnected, disabled: connection.isSwitching, - onClick: handleClickSetAsSystemProxy, + onClick: (_) async { + await ref + .read(connectionNotifierProvider.notifier) + .toggleConnection(); + }, ), MenuItem.submenu( label: t.settings.config.serviceMode, @@ -102,7 +104,7 @@ class SystemTrayController extends _$SystemTrayController (e) => MenuItem( label: e.$1, onClick: (_) async { - await ref.read(windowControllerProvider.notifier).show(); + await ref.read(windowNotifierProvider.notifier).open(); ref.read(routerProvider).go(e.$2); }, ), @@ -113,46 +115,18 @@ class SystemTrayController extends _$SystemTrayController MenuItem.separator(), MenuItem( label: t.tray.quit, - onClick: handleClickExitApp, + onClick: (_) async { + return ref.read(windowNotifierProvider.notifier).quit(); + }, ), ], ); - await trayManager.setContextMenu(trayMenu); + + await trayManager.setContextMenu(menu); } - bool _initialized = false; - - String get _trayIconPath { + static String get _trayIconPath { if (Platform.isWindows) return Assets.images.trayIconIco; return Assets.images.trayIconPng.path; } - - @override - Future onTrayIconMouseDown() async { - if (Platform.isMacOS) { - await trayManager.popUpContextMenu(); - } else { - await ref.read(windowControllerProvider.notifier).show(); - } - } - - @override - Future onTrayIconRightMouseDown() async { - super.onTrayIconRightMouseDown(); - await trayManager.popUpContextMenu(); - } - - Future handleClickShowApp(MenuItem menuItem) async { - await ref.read(windowControllerProvider.notifier).show(); - } - - Future handleClickSetAsSystemProxy(MenuItem menuItem) async { - return ref.read(connectionNotifierProvider.notifier).toggleConnection(); - } - - Future handleClickExitApp(MenuItem menuItem) async { - await ref.read(connectionNotifierProvider.notifier).abortConnection(); - await trayManager.destroy(); - return ref.read(windowControllerProvider.notifier).quit(); - } } diff --git a/lib/features/system_tray/widget/system_tray_wrapper.dart b/lib/features/system_tray/widget/system_tray_wrapper.dart new file mode 100644 index 00000000..9c40af36 --- /dev/null +++ b/lib/features/system_tray/widget/system_tray_wrapper.dart @@ -0,0 +1,53 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:hiddify/features/system_tray/notifier/system_tray_notifier.dart'; +import 'package:hiddify/features/window/notifier/window_notifier.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:tray_manager/tray_manager.dart'; + +class TrayWrapper extends StatefulHookConsumerWidget { + const TrayWrapper(this.child, {super.key}); + + final Widget child; + + @override + ConsumerState createState() => _TrayWrapperState(); +} + +class _TrayWrapperState extends ConsumerState + with TrayListener, AppLogger { + @override + Widget build(BuildContext context) { + ref.listen(systemTrayNotifierProvider, (_, __) {}); + + return widget.child; + } + + @override + void initState() { + super.initState(); + trayManager.addListener(this); + } + + @override + void dispose() { + trayManager.removeListener(this); + super.dispose(); + } + + @override + Future onTrayIconMouseDown() async { + if (Platform.isMacOS) { + await trayManager.popUpContextMenu(); + } else { + await ref.read(windowNotifierProvider.notifier).open(); + } + } + + @override + Future onTrayIconRightMouseDown() async { + await trayManager.popUpContextMenu(); + } +} diff --git a/lib/features/window/notifier/window_notifier.dart b/lib/features/window/notifier/window_notifier.dart new file mode 100644 index 00000000..8dea5958 --- /dev/null +++ b/lib/features/window/notifier/window_notifier.dart @@ -0,0 +1,54 @@ +import 'dart:ui'; + +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/model/constants.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; + +part 'window_notifier.g.dart'; + +const minimumWindowSize = Size(368, 568); +const defaultWindowSize = Size(868, 668); + +@Riverpod(keepAlive: true) +class WindowNotifier extends _$WindowNotifier with AppLogger { + @override + Future build() async { + if (!PlatformUtils.isDesktop) return; + + await windowManager.ensureInitialized(); + await windowManager.setMinimumSize(minimumWindowSize); + await windowManager.setSize(defaultWindowSize); + + final appInfo = await ref.watch(appInfoProvider.future); + await windowManager + .setTitle("${Constants.appName} v${appInfo.presentVersion}"); + } + + Future open({bool focus = true}) async { + await windowManager.show(); + if (focus) await windowManager.focus(); + } + + // TODO add option to quit or minimize to tray + Future close() async { + await windowManager.hide(); + } + + Future quit() async { + await ref + .read(connectionNotifierProvider.notifier) + .abortConnection() + .timeout(const Duration(seconds: 2)) + .catchError( + (e) { + loggy.warning("error aborting connection on quit", e); + }, + ); + await trayManager.destroy(); + await windowManager.destroy(); + } +} diff --git a/lib/features/window/widget/window_wrapper.dart b/lib/features/window/widget/window_wrapper.dart new file mode 100644 index 00000000..9f31d5d7 --- /dev/null +++ b/lib/features/window/widget/window_wrapper.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hiddify/features/window/notifier/window_notifier.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hiddify/utils/platform_utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:window_manager/window_manager.dart'; + +class WindowWrapper extends StatefulHookConsumerWidget { + const WindowWrapper(this.child, {super.key}); + + final Widget child; + + @override + ConsumerState createState() => _WindowWrapperState(); +} + +class _WindowWrapperState extends ConsumerState + with WindowListener, AppLogger { + @override + Widget build(BuildContext context) { + ref.watch(windowNotifierProvider); + + return widget.child; + } + + @override + void initState() { + super.initState(); + windowManager.addListener(this); + if (PlatformUtils.isDesktop) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await windowManager.setPreventClose(true); + }); + } + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + Future onWindowClose() async { + await ref.read(windowNotifierProvider.notifier).close(); + } + + @override + void onWindowFocus() { + setState(() {}); + } +} diff --git a/lib/features/wrapper/shortcut/shortcut_wrapper.dart b/lib/features/wrapper/shortcut/shortcut_wrapper.dart index 826d7fe6..65045043 100644 --- a/lib/features/wrapper/shortcut/shortcut_wrapper.dart +++ b/lib/features/wrapper/shortcut/shortcut_wrapper.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/features/common/window/window_controller.dart'; +import 'package:hiddify/features/window/notifier/window_notifier.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class ShortcutWrapper extends HookConsumerWidget { @@ -44,13 +44,13 @@ class ShortcutWrapper extends HookConsumerWidget { actions: { CloseWindowIntent: CallbackAction( onInvoke: (_) async { - await ref.read(windowControllerProvider.notifier).hide(); + await ref.read(windowNotifierProvider.notifier).close(); return null; }, ), QuitAppIntent: CallbackAction( onInvoke: (_) async { - await ref.read(windowControllerProvider.notifier).quit(); + await ref.read(windowNotifierProvider.notifier).quit(); return null; }, ), diff --git a/linux/my_application.cc b/linux/my_application.cc index 50af5b44..a4d15de0 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -48,7 +48,7 @@ static void my_application_activate(GApplication* application) { } gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); + gtk_widget_realize(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 33afb1be..ff1a4234 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -8,17 +8,17 @@ class AppDelegate: FlutterAppDelegate { return false } - // window manager restore from dock: https://leanflutter.dev/blog/click-dock-icon-to-restore-after-closing-the-window - override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { - if !flag { - for window in NSApp.windows { - if !window.isVisible { - window.setIsVisible(true) - } - window.makeKeyAndOrderFront(self) - NSApp.activate(ignoringOtherApps: true) - } - } - return true - } + // // window manager restore from dock: https://leanflutter.dev/blog/click-dock-icon-to-restore-after-closing-the-window + // override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + // if !flag { + // for window in NSApp.windows { + // if !window.isVisible { + // window.setIsVisible(true) + // } + // window.makeKeyAndOrderFront(self) + // NSApp.activate(ignoringOtherApps: true) + // } + // } + // return true + // } } diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 3cc05eb2..74243cee 100644 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -1,5 +1,6 @@ import Cocoa import FlutterMacOS +import window_manager class MainFlutterWindow: NSWindow { override func awakeFromNib() { @@ -12,4 +13,10 @@ class MainFlutterWindow: NSWindow { super.awakeFromNib() } -} + + // window manager hidden at launch + override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { + super.order(place, relativeTo: otherWin) + hiddenWindowAtLaunch() + } +} \ No newline at end of file diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 955ee303..d7688c5b 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -28,7 +28,8 @@ bool FlutterWindow::OnCreate() { SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); + // this->Show(); window_manager hidden at launch + "" }); // Flutter can complete the first frame before the "show window" callback is diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index 60608d0f..b2faebd7 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -135,7 +135,9 @@ bool Win32Window::Create(const std::wstring& title, double scale_factor = dpi / 96.0; HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + // window_class, title.c_str(), WS_OVERLAPPEDWINDOW, // window_manager hidden at launch + window_class, title.c_str(), + WS_OVERLAPPEDWINDOW, // do not add WS_VISIBLE since the window will be shown later Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this);