From aebb4ab73160be07cc272489c06e3e3b852ab114 Mon Sep 17 00:00:00 2001 From: 0chencc <19362246+0Chencc@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:50:35 +0800 Subject: [PATCH] fix: resolve macOS tray click crash - Use system_tray's AppWindow instead of window_manager for showing window on macOS to avoid nil pointer crash in WindowManager.swift - Set applicationShouldTerminateAfterLastWindowClosed to false to keep app running when minimized to tray - Save ref.listenManual() subscription to prevent GC --- lib/app.dart | 12 ++++++---- lib/core/services/system_tray_service.dart | 28 +++++++++++++++------- macos/Runner/AppDelegate.swift | 3 ++- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index e3b3cba..4fc7d9f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -9,6 +9,7 @@ import 'application/providers/credentials_provider.dart'; import 'application/providers/locale_provider.dart'; import 'application/providers/navigation_provider.dart'; import 'application/providers/playback_provider.dart'; +import 'domain/entities/playback_state.dart'; import 'core/services/system_tray_service.dart'; import 'presentation/screens/home_screen.dart'; import 'presentation/screens/login_screen.dart'; @@ -55,6 +56,7 @@ class _AppShellState extends ConsumerState<_AppShell> with WindowListener { final _systemTray = SystemTrayService(); final _navigatorKey = GlobalKey(); bool _isExiting = false; + ProviderSubscription? _playbackSubscription; @override void initState() { @@ -80,6 +82,7 @@ class _AppShellState extends ConsumerState<_AppShell> with WindowListener { @override void dispose() { + _playbackSubscription?.close(); windowManager.removeListener(this); _systemTray.dispose(); super.dispose(); @@ -124,11 +127,12 @@ class _AppShellState extends ConsumerState<_AppShell> with WindowListener { ); // Listen to playback state changes to update tray - ref.listenManual(playbackProvider, (previous, next) { + _playbackSubscription = ref.listenManual(playbackProvider, (previous, next) { + final state = next as PlaybackState; _systemTray.updatePlaybackState( - isPlaying: next.isPlaying, - trackName: next.currentTrack?.name, - artistName: next.currentTrack?.artistNames, + isPlaying: state.isPlaying, + trackName: state.currentTrack?.name, + artistName: state.currentTrack?.artistNames, ); }); } diff --git a/lib/core/services/system_tray_service.dart b/lib/core/services/system_tray_service.dart index b54915c..844b948 100644 --- a/lib/core/services/system_tray_service.dart +++ b/lib/core/services/system_tray_service.dart @@ -50,6 +50,7 @@ class SystemTrayService { SystemTrayService._internal(); final SystemTray _systemTray = SystemTray(); + final AppWindow _appWindow = AppWindow(); bool _isInitialized = false; TrayCallback? _onShowWindow; @@ -275,18 +276,29 @@ class SystemTrayService { /// Show the main window /// Uses opacity trick to prevent transparent window flash on restore Future showWindow() async { - // Set opacity to 0 before showing to prevent transparent flash - await windowManager.setOpacity(0); - await windowManager.show(); - await windowManager.focus(); - // Small delay to let Flutter render, then restore opacity - await Future.delayed(const Duration(milliseconds: 50)); - await windowManager.setOpacity(1); + if (Platform.isMacOS) { + // On macOS, use system_tray's AppWindow to avoid window_manager nil crash + await _appWindow.show(); + } else { + // On Windows/Linux, use window_manager with opacity trick + await windowManager.setOpacity(0); + await windowManager.show(); + await windowManager.focus(); + // Small delay to let Flutter render, then restore opacity + await Future.delayed(const Duration(milliseconds: 50)); + await windowManager.setOpacity(1); + } } /// Hide the window to tray Future hideToTray() async { - await windowManager.hide(); + try { + await windowManager.hide(); + } catch (e) { + // Fallback: use AppWindow.hide() which minimizes on macOS + AppLogger.warning('windowManager.hide() failed, using fallback', e); + await _appWindow.hide(); + } } /// Dispose of tray resources diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 363a0a6..fd4b880 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -16,7 +16,8 @@ class AppDelegate: FlutterAppDelegate { } override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true + // Return false to allow the app to keep running when minimized to tray + return false } override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {