Prevent unconditional frameless calls on non-Wayland platforms. Signed-off-by: Texas0295 <kimura@texas0295.top>
		
			
				
	
	
		
			326 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			326 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:io';
 | |
| import 'package:croppy/croppy.dart';
 | |
| import 'package:easy_localization/easy_localization.dart' hide TextDirection;
 | |
| import 'package:firebase_core/firebase_core.dart';
 | |
| import 'package:firebase_crashlytics/firebase_crashlytics.dart';
 | |
| import 'package:firebase_messaging/firebase_messaging.dart';
 | |
| import 'package:flutter/foundation.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hooks/flutter_hooks.dart';
 | |
| import 'package:go_router/go_router.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:image_picker_android/image_picker_android.dart';
 | |
| import 'package:island/talker.dart';
 | |
| import 'package:island/firebase_options.dart';
 | |
| import 'package:island/pods/config.dart';
 | |
| import 'package:island/pods/network.dart';
 | |
| import 'package:island/pods/theme.dart';
 | |
| import 'package:island/pods/userinfo.dart';
 | |
| import 'package:island/pods/websocket.dart';
 | |
| import 'package:island/route.dart';
 | |
| import 'package:island/services/notify.dart';
 | |
| import 'package:island/services/timezone.dart';
 | |
| import 'package:island/widgets/alert.dart';
 | |
| import 'package:island/widgets/app_scaffold.dart';
 | |
| import 'package:relative_time/relative_time.dart';
 | |
| import 'package:shared_preferences/shared_preferences.dart';
 | |
| import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
 | |
| import 'package:flutter_native_splash/flutter_native_splash.dart';
 | |
| import 'package:talker_flutter/talker_flutter.dart';
 | |
| import 'package:talker_riverpod_logger/talker_riverpod_logger.dart';
 | |
| import 'package:url_launcher/url_launcher_string.dart';
 | |
| import 'package:window_manager/window_manager.dart';
 | |
| 
 | |
| @pragma('vm:entry-point')
 | |
| Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
 | |
|   await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
 | |
|   talker.info('Handling a background message: ${message.messageId}');
 | |
| }
 | |
| 
 | |
| void main() async {
 | |
|   final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
 | |
|   if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
 | |
|     talker.info(
 | |
|       "[SplashScreen] Keeping the flash screen to loading other resources...",
 | |
|     );
 | |
|     FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
 | |
|   }
 | |
| 
 | |
|   if (kIsWeb) {
 | |
|     GoRouter.optionURLReflectsImperativeAPIs = true;
 | |
|   }
 | |
| 
 | |
|   try {
 | |
|     await EasyLocalization.ensureInitialized();
 | |
| 
 | |
|     if (kIsWeb || !Platform.isLinux) {
 | |
|       await Firebase.initializeApp(
 | |
|         options: DefaultFirebaseOptions.currentPlatform,
 | |
|       );
 | |
|       FirebaseMessaging.onBackgroundMessage(
 | |
|         _firebaseMessagingBackgroundHandler,
 | |
|       );
 | |
|       // Although previous if case checked this. Still check is web or not
 | |
|       // Otherwise the web platform will broke due to there is no Platform api on the web
 | |
|       // Skip crashlytics setup on debug mode to prevent unexpected report to firebase
 | |
|       if ((kIsWeb || !Platform.isWindows) && !kDebugMode) {
 | |
|         FlutterError.onError =
 | |
|             FirebaseCrashlytics.instance.recordFlutterFatalError;
 | |
|         PlatformDispatcher.instance.onError = (error, stack) {
 | |
|           FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
 | |
|           return true;
 | |
|         };
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     talker.info("[SplashScreen] Firebase is ready!");
 | |
|   } catch (err) {
 | |
|     showErrorAlert(err);
 | |
|   }
 | |
| 
 | |
|   try {
 | |
|     talker.info("[SplashScreen] Loading timezone database...");
 | |
|     await initializeTzdb();
 | |
|     talker.info("[SplashScreen] Time zone database was loaded!");
 | |
|   } catch (err) {
 | |
|     talker.error("[SplashScreen] Failed to load timezone database... $err");
 | |
|   }
 | |
| 
 | |
|   final prefs = await SharedPreferences.getInstance();
 | |
| 
 | |
|   if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
 | |
|     await windowManager.ensureInitialized();
 | |
| 
 | |
|     const defaultSize = Size(360, 640);
 | |
| 
 | |
|     // Get saved window size from preferences
 | |
|     final savedSizeString = prefs.getString(kAppWindowSize);
 | |
|     Size initialSize = defaultSize;
 | |
| 
 | |
|     if (savedSizeString != null) {
 | |
|       try {
 | |
|         final parts = savedSizeString.split(',');
 | |
|         if (parts.length == 2) {
 | |
|           final width = double.parse(parts[0]);
 | |
|           final height = double.parse(parts[1]);
 | |
|           initialSize = Size(width, height);
 | |
|         }
 | |
|       } catch (e) {
 | |
|         talker.error("[SplashScreen] Failed to parse saved window size: $e");
 | |
|         initialSize = defaultSize;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     WindowOptions windowOptions = WindowOptions(
 | |
|       size: initialSize,
 | |
|       center: true,
 | |
|       backgroundColor: Colors.transparent,
 | |
|       skipTaskbar: false,
 | |
|       titleBarStyle: TitleBarStyle.hidden,
 | |
|       windowButtonVisibility: true,
 | |
|     );
 | |
|     windowManager.waitUntilReadyToShow(windowOptions, () async {
 | |
|       final env = Platform.environment;
 | |
|       final isWayland = env.containsKey('WAYLAND_DISPLAY');
 | |
| 
 | |
|       if (isWayland) {
 | |
|         try {
 | |
|           await windowManager.setAsFrameless();
 | |
|         } catch (e) {
 | |
|           debugPrint('[Wayland] setAsFrameless failed: $e');
 | |
|         }
 | |
|       }
 | |
|       await windowManager.setMinimumSize(defaultSize);
 | |
|       await windowManager.show();
 | |
|       await windowManager.focus();
 | |
|       final opacity = prefs.getDouble(kAppWindowOpacity) ?? 1.0;
 | |
|       await windowManager.setOpacity(opacity);
 | |
|       talker.info(
 | |
|         "[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}"
 | |
|         "${isWayland ? " (Wayland frameless fix applied)" : ""}",
 | |
|       );
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   if (!kIsWeb && Platform.isAndroid) {
 | |
|     final ImagePickerPlatform imagePickerImplementation =
 | |
|         ImagePickerPlatform.instance;
 | |
|     if (imagePickerImplementation is ImagePickerAndroid) {
 | |
|       imagePickerImplementation.useAndroidPhotoPicker = true;
 | |
|     }
 | |
|     talker.info("[SplashScreen] Android image picker is ready!");
 | |
|   }
 | |
| 
 | |
|   if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
 | |
|     FlutterNativeSplash.remove();
 | |
|     talker.info("[SplashScreen] Now hiding the splash screen...");
 | |
|   }
 | |
| 
 | |
|   runApp(
 | |
|     ProviderScope(
 | |
|       observers: [
 | |
|         TalkerRiverpodObserver(
 | |
|           talker: talker,
 | |
|           settings: TalkerRiverpodLoggerSettings(
 | |
|             printProviderAdded: false,
 | |
|             printProviderDisposed: false,
 | |
|             printProviderUpdated: false,
 | |
|             printStateFullData: false,
 | |
|           ),
 | |
|         ),
 | |
|       ],
 | |
|       overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
 | |
|       child: Directionality(
 | |
|         textDirection: TextDirection.ltr,
 | |
|         child: EasyLocalization(
 | |
|           supportedLocales: [
 | |
|             Locale('en', 'US'),
 | |
|             Locale('zh', 'CN'),
 | |
|             Locale('zh', 'TW'),
 | |
|             Locale('zh', 'OG'),
 | |
|             Locale('ja', 'JP'),
 | |
|             Locale('ko', 'KR'),
 | |
|             Locale('es', 'ES'),
 | |
|           ],
 | |
|           path: 'assets/i18n',
 | |
|           fallbackLocale: Locale('en', 'US'),
 | |
|           useFallbackTranslations: true,
 | |
|           child: IslandApp(),
 | |
|         ),
 | |
|       ),
 | |
|     ),
 | |
|   );
 | |
| }
 | |
| 
 | |
| // Router will be provided through Riverpod
 | |
| 
 | |
| final globalOverlay = GlobalKey<OverlayState>();
 | |
| 
 | |
| class IslandApp extends HookConsumerWidget {
 | |
|   const IslandApp({super.key});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final theme = ref.watch(themeProvider);
 | |
|     final settings = ref.watch(appSettingsNotifierProvider);
 | |
| 
 | |
|     // Convert string theme mode to ThemeMode enum
 | |
|     ThemeMode getThemeMode() {
 | |
|       final themeMode = settings.themeMode ?? 'system';
 | |
|       switch (themeMode) {
 | |
|         case 'light':
 | |
|           return ThemeMode.light;
 | |
|         case 'dark':
 | |
|           return ThemeMode.dark;
 | |
|         case 'system':
 | |
|         default:
 | |
|           return ThemeMode.system;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     void handleMessage(RemoteMessage notification) {
 | |
|       if (notification.data['meta']?['action_uri'] != null) {
 | |
|         var uri = notification.data['meta']['action_uri'] as String;
 | |
|         if (uri.startsWith('/')) {
 | |
|           // In-app routes
 | |
|           final router = ref.read(routerProvider);
 | |
|           router.push(notification.data['meta']['action_uri']);
 | |
|         } else {
 | |
|           // External links
 | |
|           launchUrlString(uri);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     useEffect(() {
 | |
|       if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       // When the app is opened from a terminated state.
 | |
|       FirebaseMessaging.instance.getInitialMessage().then((message) {
 | |
|         if (message != null) {
 | |
|           handleMessage(message);
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       // When the app is in the background and opened.
 | |
|       final onMessageOpenedAppSubscription = FirebaseMessaging
 | |
|           .onMessageOpenedApp
 | |
|           .listen(handleMessage);
 | |
| 
 | |
|       // When the app is in the foreground.
 | |
|       final onMessageSubscription = FirebaseMessaging.onMessage.listen((
 | |
|         message,
 | |
|       ) {
 | |
|         talker.info(
 | |
|           '[Notification] foreground message received: ${message.messageId}',
 | |
|         );
 | |
|         handleMessage(message);
 | |
|       });
 | |
| 
 | |
|       return () {
 | |
|         onMessageOpenedAppSubscription.cancel();
 | |
|         onMessageSubscription.cancel();
 | |
|       };
 | |
|     }, []);
 | |
| 
 | |
|     useEffect(() {
 | |
|       // Load userinfo
 | |
|       final userNotifier = ref.read(userInfoProvider.notifier);
 | |
|       ref.listen(websocketStateProvider, (_, state) {
 | |
|         talker.info('[WebSocket] $state');
 | |
|       });
 | |
|       Future(() {
 | |
|         userNotifier.fetchUser().then((_) {
 | |
|           final user = ref.watch(userInfoProvider);
 | |
|           if (user.value != null) {
 | |
|             final apiClient = ref.read(apiClientProvider);
 | |
|             subscribePushNotification(apiClient);
 | |
|             initializeLocalNotifications();
 | |
|             final wsNotifier = ref.read(websocketStateProvider.notifier);
 | |
|             wsNotifier.connect();
 | |
|           }
 | |
|         });
 | |
|       });
 | |
|       return null;
 | |
|     }, []);
 | |
| 
 | |
|     final router = ref.watch(routerProvider);
 | |
| 
 | |
|     return MaterialApp.router(
 | |
|       color: Colors.transparent,
 | |
|       theme: theme.light,
 | |
|       darkTheme: theme.dark,
 | |
|       themeMode: getThemeMode(),
 | |
|       routerConfig: router,
 | |
|       supportedLocales: context.supportedLocales,
 | |
|       scrollBehavior: AppScrollBehavior(),
 | |
|       localizationsDelegates: [
 | |
|         ...context.localizationDelegates,
 | |
|         CroppyLocalizations.delegate,
 | |
|         RelativeTimeLocalizations.delegate,
 | |
|       ],
 | |
|       locale: context.locale,
 | |
|       builder: (context, child) {
 | |
|         return Overlay(
 | |
|           key: globalOverlay,
 | |
|           initialEntries: [
 | |
|             OverlayEntry(
 | |
|               builder: (_) {
 | |
|                 return TalkerWrapper(
 | |
|                   talker: talker,
 | |
|                   options: const TalkerWrapperOptions(enableErrorAlerts: true),
 | |
|                   child: WindowScaffold(
 | |
|                     child: child ?? const SizedBox.shrink(),
 | |
|                   ),
 | |
|                 );
 | |
|               },
 | |
|             ),
 | |
|           ],
 | |
|         );
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| }
 |