From fe90c1d2610b04d04f09dd986a52a987188fd23e Mon Sep 17 00:00:00 2001 From: Levi Date: Wed, 28 Jan 2026 18:57:09 +0800 Subject: [PATCH] fix: deep link hidden handling on MacOS (#1262) --- .../app/controllers/app_controller.dart | 51 ++++++++++++----- .../lib/app/modules/app/views/app_view.dart | 6 +- .../extension/views/extension_view.dart | 28 +++++---- ui/flutter/lib/main.dart | 57 +++++++------------ 4 files changed, 82 insertions(+), 60 deletions(-) diff --git a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart index 19a28803..ebf22b92 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -62,6 +62,11 @@ final allTrackerSubscribeUrlCdns = { class AppController extends GetxController with WindowListener, TrayListener { static StartConfig? _defaultStartConfig; + /// Command line --hidden flag passed from main.dart + final bool hiddenFromArgs; + + AppController({this.hiddenFromArgs = false}); + final autoStartup = false.obs; final startConfig = StartConfig().obs; final runningPort = 0.obs; @@ -156,24 +161,44 @@ class AppController extends GetxController with WindowListener, TrayListener { Future _initDeepLinks() async { if (Util.isWeb()) { + // For web, just show window return; } // Handle deep link - () async { - _appLinks = AppLinks(); + _appLinks = AppLinks(); - // Handle link when app is in warm state (front or background) - _linkSubscription = _appLinks.uriLinkStream.listen((uri) async { - await _handleDeepLink(uri); - }); + // Handle link when app is in warm state (front or background) + _linkSubscription = _appLinks.uriLinkStream.listen((uri) async { + await _handleDeepLink(uri); + }); - // Check initial link if app was in cold state (terminated) - final uri = await _appLinks.getInitialLink(); - if (uri != null) { - await _handleDeepLink(uri); - } - }(); + // Get initial link for cold start - this works after runApp() + Uri? initialLink; + try { + initialLink = await _appLinks.getInitialLink(); + } catch (e) { + // ignore errors + } + + // Determine if window should be hidden + // Priority 1: gopeed: URL scheme hidden parameter + // Priority 2: Command line --hidden flag + bool shouldHide = hiddenFromArgs; + if (initialLink?.scheme == "gopeed") { + shouldHide = initialLink!.queryParameters["hidden"] == "true"; + } + + // Show window if not hidden (desktop only) + if (Util.isDesktop() && !shouldHide) { + await windowManager.show(); + await windowManager.focus(); + } + + // Handle initial link for deep link navigation + if (initialLink != null) { + _handleDeepLink(initialLink); + } // Handle shared media, e.g. shared link from browser if (Util.isMobile()) { @@ -638,7 +663,7 @@ class AppController extends GetxController with WindowListener, TrayListener { launchAtStartup.setup( appName: packageInfo.appName, appPath: Platform.resolvedExecutable, - args: ['--${Args.flagHidden}']); + args: ['--${StartupArgs.flagHidden}']); autoStartup.value = await launchAtStartup.isEnabled(); } diff --git a/ui/flutter/lib/app/modules/app/views/app_view.dart b/ui/flutter/lib/app/modules/app/views/app_view.dart index e279a6a5..80a1e8ec 100644 --- a/ui/flutter/lib/app/modules/app/views/app_view.dart +++ b/ui/flutter/lib/app/modules/app/views/app_view.dart @@ -44,7 +44,11 @@ class AppView extends GetView { // Set the title bar to use the actual brightness of the UI windowManager.setBrightness(brightness); } - return child!; + // Fix for GetX Overlay issue with Flutter 3.38.1+ + // Reference: https://github.com/jonataslaw/getx/issues/3425 + return Overlay( + initialEntries: [OverlayEntry(builder: (_) => child!)], + ); }, ), ); diff --git a/ui/flutter/lib/app/modules/extension/views/extension_view.dart b/ui/flutter/lib/app/modules/extension/views/extension_view.dart index c3987efe..a21ec075 100644 --- a/ui/flutter/lib/app/modules/extension/views/extension_view.dart +++ b/ui/flutter/lib/app/modules/extension/views/extension_view.dart @@ -38,7 +38,7 @@ class ExtensionView extends GetView { _installBtnController.start(); try { await installExtension(InstallExtension(url: _installUrlController.text)); - Get.snackbar('tip'.tr, 'extensionInstallSuccess'.tr); + showMessage('tip'.tr, 'extensionInstallSuccess'.tr); await controller.load(); } catch (e) { showErrorMessage(e); @@ -109,7 +109,7 @@ class ExtensionView extends GetView { try { await installExtension( InstallExtension(devMode: true, url: dir)); - Get.snackbar( + showMessage( 'tip'.tr, 'extensionInstallSuccess'.tr); await controller.load(); } catch (e) { @@ -290,7 +290,7 @@ class ExtensionView extends GetView { return showDialog( context: Get.context!, barrierDismissible: false, - builder: (_) => AlertDialog( + builder: (dialogContext) => AlertDialog( content: Builder(builder: (context) { final height = MediaQuery.of(context).size.height; final width = MediaQuery.of(context).size.width; @@ -351,7 +351,7 @@ class ExtensionView extends GetView { backgroundColor: MaterialStateProperty.all( Get.theme.colorScheme.background)), onPressed: () { - Get.back(); + Navigator.of(dialogContext).pop(); }, child: Text('cancel'.tr), ), @@ -372,7 +372,9 @@ class ExtensionView extends GetView { UpdateExtensionSettings( settings: formKey.currentState!.value)); await controller.load(); - Get.back(); + if (dialogContext.mounted) { + Navigator.of(dialogContext).pop(); + } } } catch (e) { showErrorMessage(e); @@ -453,12 +455,12 @@ class ExtensionView extends GetView { showDialog( context: Get.context!, barrierDismissible: false, - builder: (_) => AlertDialog( + builder: (dialogContext) => AlertDialog( title: Text('extensionDelete'.tr), actions: [ TextButton( child: Text('cancel'.tr), - onPressed: () => Get.back(), + onPressed: () => Navigator.of(dialogContext).pop(), ), TextButton( child: Text( @@ -469,7 +471,9 @@ class ExtensionView extends GetView { try { await deleteExtension(extension.identity); await controller.load(); - Get.back(); + if (dialogContext.mounted) { + Navigator.of(dialogContext).pop(); + } } catch (e) { showErrorMessage(e); } @@ -484,14 +488,14 @@ class ExtensionView extends GetView { showDialog( context: Get.context!, - builder: (context) => AlertDialog( + builder: (dialogContext) => AlertDialog( content: Text('newVersionTitle'.trParams({ 'version': 'v${controller.updateFlags[extension.identity]!}' })), actions: [ TextButton( onPressed: () { - Get.back(); + Navigator.of(dialogContext).pop(); }, child: Text('newVersionLater'.tr), ), @@ -503,7 +507,9 @@ class ExtensionView extends GetView { await updateExtension(extension.identity); await controller.load(); controller.updateFlags.remove(extension.identity); - Get.back(); + if (dialogContext.mounted) { + Navigator.of(dialogContext).pop(); + } showMessage('tip'.tr, 'extensionUpdateSuccess'.tr); } catch (e) { showErrorMessage(e); diff --git a/ui/flutter/lib/main.dart b/ui/flutter/lib/main.dart index d5dfa43a..ac1bf1e4 100644 --- a/ui/flutter/lib/main.dart +++ b/ui/flutter/lib/main.dart @@ -22,37 +22,32 @@ import 'util/scheme_register/scheme_register.dart'; import 'util/updater.dart'; import 'util/util.dart'; -class Args { +class StartupArgs { static const flagHidden = "hidden"; - bool hidden = false; + /// Command line --hidden flag (for auto-start) + bool hiddenFromArgs = false; - Args(); + StartupArgs._(); - Args.parse(List args) { - final parser = ArgParser(); - parser.addFlag(flagHidden); - final results = parser.parse(args); - hidden = results.flag(flagHidden); + /// Parse from command line arguments only + static StartupArgs parse(List arguments) { + final args = StartupArgs._(); + try { + final parser = ArgParser()..addFlag(flagHidden); + final results = parser.parse(arguments); + args.hiddenFromArgs = results.flag(flagHidden); + } catch (e) { + // ignore parse errors + } + return args; } } void main(List arguments) async { - Args args; - // Parse url scheme arguments, e.g. gopeed:?hidden=true - // TODO: macos open url handle - // TODO: macos updater test - if (arguments.firstOrNull?.startsWith("gopeed:") == true) { - try { - final uri = Uri.parse(arguments.first); - args = Args()..hidden = uri.queryParameters["hidden"] == "true"; - } catch (e) { - // ignore - args = Args.parse([]); - } - } else { - args = Args.parse(arguments); - } + WidgetsFlutterBinding.ensureInitialized(); + + final args = StartupArgs.parse(arguments); await init(args); onStart(); @@ -60,8 +55,8 @@ void main(List arguments) async { runApp(const AppView()); } -Future init(Args args) async { - WidgetsFlutterBinding.ensureInitialized(); +Future init(StartupArgs args) async { + // Note: WidgetsFlutterBinding.ensureInitialized() is already called in main() if (Util.isMobile()) { FlutterForegroundTask.initCommunicationPort(); } @@ -81,16 +76,7 @@ Future init(Args args) async { skipTaskbar: runAsMenubarApp, ); await windowManager.waitUntilReadyToShow(windowOptions, () async { - if (!args.hidden) { - await windowManager.show(); - await windowManager.focus(); - } await windowManager.setPreventClose(true); - // windows_manager has a bug where when window to be maximized, it will be unmaximized immediately, so can't implement this feature currently. - // https://github.com/leanflutter/window_manager/issues/412 - // if (windowState.isMaximized) { - // await windowManager.maximize(); - // } }); // Register Cmd+W hotkey on macOS to close window @@ -118,7 +104,8 @@ Future init(Args args) async { logger.e("init package info fail", e); } - final controller = Get.put(AppController()); + final controller = + Get.put(AppController(hiddenFromArgs: args.hiddenFromArgs)); try { await controller.loadStartConfig(); final startCfg = controller.startConfig.value;