import 'dart:async'; import 'dart:io'; import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:equatable/equatable.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:hacki/blocs/blocs.dart'; import 'package:hacki/config/constants.dart'; import 'package:hacki/config/custom_router.dart'; import 'package:hacki/config/locator.dart'; import 'package:hacki/cubits/cubits.dart'; import 'package:hacki/services/fetcher.dart'; import 'package:hacki/styles/styles.dart'; import 'package:hacki/utils/haptic_feedback_util.dart'; import 'package:hacki/utils/theme_util.dart'; import 'package:hive/hive.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:logger/logger.dart'; import 'package:path_provider/path_provider.dart'; import 'package:rxdart/rxdart.dart' show BehaviorSubject; import 'package:visibility_detector/visibility_detector.dart'; import 'package:workmanager/workmanager.dart'; // For receiving payload event from local notifications. final BehaviorSubject selectNotificationSubject = BehaviorSubject(); // For receiving payload event from siri suggestions. final BehaviorSubject siriSuggestionSubject = BehaviorSubject(); late final bool isTesting; void notificationReceiver(NotificationResponse details) => selectNotificationSubject.add(details.payload); Future main({bool testing = false}) async { WidgetsFlutterBinding.ensureInitialized(); await initializeDateFormatting(Platform.localeName); isTesting = testing; final Directory tempDir = await getTemporaryDirectory(); final String tempPath = tempDir.path; Hive.init(tempPath); final HydratedStorage storage = await HydratedStorage.build( storageDirectory: HydratedStorageDirectory(tempPath), ); HydratedBloc.storage = storage; await setUpLocator(); EquatableConfig.stringify = true; FlutterError.onError = (FlutterErrorDetails details) { locator.get().e( details.summary, error: details.exceptionAsString(), stackTrace: details.stack, ); }; if (Platform.isIOS) { unawaited( Workmanager().initialize( fetcherCallbackDispatcher, ), ); final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); const DarwinInitializationSettings initializationSettingsIOS = DarwinInitializationSettings(); const InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, iOS: initializationSettingsIOS, ); await flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveBackgroundNotificationResponse: notificationReceiver, onDidReceiveNotificationResponse: notificationReceiver, ); await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() ?.requestPermissions( alert: true, badge: true, sound: true, ); } else if (Platform.isAndroid) { final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo; final int sdk = androidInfo.version.sdkInt; if (sdk > 28) { SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: Palette.transparent, systemNavigationBarColor: Palette.transparent, systemNavigationBarDividerColor: Palette.transparent, ), ); } await SystemChrome.setEnabledSystemUIMode( SystemUiMode.edgeToEdge, overlays: [SystemUiOverlay.top], ); } final AdaptiveThemeMode? savedThemeMode = await AdaptiveTheme.getThemeMode(); // Uncomment this line to log events from bloc/cubit. // Bloc.observer = CustomBlocObserver(); VisibilityDetectorController.instance.updateInterval = AppDurations.ms200; runApp( HackiApp( savedThemeMode: savedThemeMode, ), ); } class HackiApp extends StatelessWidget { const HackiApp({ super.key, this.savedThemeMode, }); final AdaptiveThemeMode? savedThemeMode; @override Widget build(BuildContext context) { return MultiBlocProvider( providers: >[ BlocProvider.value( value: locator.get(), ), BlocProvider( lazy: false, create: (BuildContext context) => PreferenceCubit(), ), BlocProvider( lazy: false, create: (BuildContext context) => FilterCubit(), ), BlocProvider( create: (BuildContext context) => StoriesBloc( preferenceCubit: context.read(), filterCubit: context.read(), )..add(StoriesInitialize(startup: true)), ), BlocProvider( lazy: false, create: (BuildContext context) => AuthBloc(), ), BlocProvider( lazy: false, create: (BuildContext context) => HistoryCubit( authBloc: context.read(), ), ), BlocProvider( lazy: false, create: (BuildContext context) => FavCubit( authBloc: context.read(), ), ), BlocProvider( lazy: false, create: (BuildContext context) => BlocklistCubit(), ), BlocProvider( lazy: false, create: (BuildContext context) => SearchCubit(), ), BlocProvider( lazy: false, create: (BuildContext context) => NotificationCubit( authBloc: context.read(), preferenceCubit: context.read(), ), ), BlocProvider( lazy: false, create: (BuildContext context) => PinCubit(), ), BlocProvider( lazy: false, create: (BuildContext context) => SplitViewCubit(), ), BlocProvider( lazy: false, create: (BuildContext context) => ReminderCubit(), ), BlocProvider( lazy: false, create: (BuildContext context) => PostCubit(), ), BlocProvider( lazy: false, create: (BuildContext context) => EditCubit(), ), BlocProvider( create: (BuildContext context) => TabCubit( preferenceCubit: context.read(), ), ), ], child: BlocConsumer( listenWhen: (PreferenceState previous, PreferenceState current) => previous.isHapticFeedbackEnabled != current.isHapticFeedbackEnabled, listener: (_, PreferenceState state) { HapticFeedbackUtil.enabled = state.isHapticFeedbackEnabled; }, buildWhen: (PreferenceState previous, PreferenceState current) => previous.appColor != current.appColor || previous.font != current.font || previous.textScaleFactor != current.textScaleFactor || previous.isTrueDarkModeEnabled != current.isTrueDarkModeEnabled, builder: (BuildContext context, PreferenceState state) { return AdaptiveTheme( key: ValueKey( '''${state.appColor}${state.font}${state.isTrueDarkModeEnabled}''', ), light: ThemeData( primaryColor: state.appColor, colorScheme: ColorScheme.fromSwatch( primarySwatch: state.appColor, ), fontFamily: state.font.name, ), dark: ThemeData( brightness: Brightness.dark, primaryColor: state.appColor, colorScheme: ColorScheme.fromSwatch( primarySwatch: state.appColor, brightness: Brightness.dark, ), fontFamily: state.font.name, ), initial: savedThemeMode ?? AdaptiveThemeMode.system, builder: (ThemeData theme, ThemeData darkTheme) { return FutureBuilder( future: AdaptiveTheme.getThemeMode(), builder: ( BuildContext context, AsyncSnapshot snapshot, ) { final AdaptiveThemeMode? mode = snapshot.data; ThemeUtil.updateStatusBarSetting( SchedulerBinding .instance.platformDispatcher.platformBrightness, mode, ); final bool isDarkModeEnabled = () { if (mode == null) { return View.of(context) .platformDispatcher .platformBrightness == Brightness.dark; } else { return mode == AdaptiveThemeMode.dark || (mode == AdaptiveThemeMode.system && View.of(context) .platformDispatcher .platformBrightness == Brightness.dark); } }(); final ColorScheme colorScheme = ColorScheme.fromSeed( brightness: isDarkModeEnabled ? Brightness.dark : Brightness.light, seedColor: state.appColor, ); return FeatureDiscovery( child: MediaQuery( data: state.textScaleFactor == 1 ? MediaQuery.of(context) : MediaQuery.of(context).copyWith( textScaler: TextScaler.linear( state.textScaleFactor, ), ), child: MaterialApp.router( title: 'Hacki', debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: colorScheme, fontFamily: state.font.name, canvasColor: isDarkModeEnabled && state.isTrueDarkModeEnabled ? Palette.black : null, scaffoldBackgroundColor: isDarkModeEnabled && state.isTrueDarkModeEnabled ? Palette.black : null, dividerTheme: DividerThemeData( color: Palette.grey.withValues(alpha: 0.2), ), switchTheme: SwitchThemeData( trackColor: WidgetStateProperty.resolveWith( (Set states) { if (states.contains(WidgetState.selected)) { return colorScheme.primary .withValues(alpha: 0.6); } else { return Palette.grey.withValues(alpha: 0.2); } }, ), ), bottomSheetTheme: const BottomSheetThemeData( modalElevation: 8, clipBehavior: Clip.hardEdge, shadowColor: Palette.black, ), inputDecorationTheme: InputDecorationTheme( enabledBorder: UnderlineInputBorder( borderSide: BorderSide( color: isDarkModeEnabled ? Palette.white : Palette.black, ), ), disabledBorder: UnderlineInputBorder( borderSide: BorderSide( color: (isDarkModeEnabled ? Palette.white : Palette.black) .withValues(alpha: 0.4), ), ), ), sliderTheme: SliderThemeData( inactiveTrackColor: colorScheme.primary.withValues(alpha: 0.5), activeTrackColor: colorScheme.primary, thumbColor: colorScheme.primary, ), outlinedButtonTheme: OutlinedButtonThemeData( style: ButtonStyle( side: WidgetStateBorderSide.resolveWith( (_) => const BorderSide( color: Palette.grey, ), ), ), ), ), routerConfig: router, ), ), ); }, ); }, ); }, ), ); } }