main.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import 'dart:async';
  2. import 'dart:developer' as dev;
  3. import 'dart:io';
  4. import 'package:device_info_plus/device_info_plus.dart';
  5. import 'package:firebase_core/firebase_core.dart';
  6. import 'package:firebase_crashlytics/firebase_crashlytics.dart';
  7. import 'package:flutter/foundation.dart';
  8. import 'package:flutter/material.dart';
  9. import 'package:flutter/services.dart';
  10. import 'package:logging/logging.dart';
  11. import 'package:path_provider/path_provider.dart';
  12. import 'package:provider/provider.dart';
  13. import 'package:puzzleweave/ads/applovin_ads_controller.dart';
  14. import 'package:puzzleweave/app_lifecycle/app_lifecycle.dart';
  15. import 'package:puzzleweave/audio/jc_audio_controller.dart';
  16. import 'package:puzzleweave/firebase/firebase_options.dart';
  17. import 'package:puzzleweave/homepage/home_screen.dart';
  18. import 'package:puzzleweave/l10n/app_localizations.dart';
  19. import 'package:puzzleweave/models/data.dart';
  20. import 'package:puzzleweave/models/items.dart';
  21. import 'package:puzzleweave/persistence/persistence.dart';
  22. import 'package:puzzleweave/play/board_play.dart';
  23. import 'package:puzzleweave/remote_config/remote_config.dart';
  24. import 'package:puzzleweave/settings/settings_controller.dart';
  25. import 'package:puzzleweave/utils/utils.dart';
  26. import 'config/config.dart' as cfg;
  27. import 'config/device.dart';
  28. Logger _log = Logger('main.dart');
  29. final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
  30. void main() async {
  31. // Subscribe to log messages.
  32. Logger.root.onRecord.listen((record) {
  33. dev.log(
  34. record.message,
  35. time: record.time,
  36. level: record.level.value,
  37. name: record.loggerName,
  38. zone: record.zone,
  39. error: record.error,
  40. stackTrace: record.stackTrace,
  41. );
  42. });
  43. WidgetsFlutterBinding.ensureInitialized();
  44. // 强制竖屏
  45. SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
  46. // 进入全屏沉浸式, 隐藏底部导航以及状态栏
  47. if (Platform.isAndroid) {
  48. SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
  49. }
  50. SystemChrome.setSystemUIOverlayStyle(
  51. const SystemUiOverlayStyle(
  52. statusBarColor: Colors.transparent, // <-- SEE HERE
  53. statusBarIconBrightness: Brightness.dark, //<-- For Android SEE HERE (dark icons)
  54. statusBarBrightness: Brightness.light, //<-- For iOS SEE HERE (dark icons)
  55. ),
  56. );
  57. ////////////////////// firebase relate ///////////////////////////////
  58. if (!kIsWeb && (Platform.isAndroid)) {
  59. try {
  60. await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  61. FlutterError.onError = (errorDetails) {
  62. FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
  63. };
  64. // Pass all uncaught asynchronous errors
  65. // that aren't handled by the Flutter framework to Crashlytics.
  66. PlatformDispatcher.instance.onError = (error, stack) {
  67. final errorStr = error.toString();
  68. // 1. 扩展网络/环境类异常的拦截范围
  69. final bool isEnvironmentIssue =
  70. error is SocketException ||
  71. error is HttpException ||
  72. error is HandshakeException ||
  73. errorStr.contains('ClientException') ||
  74. errorStr.contains('SocketException') ||
  75. errorStr.contains('HandshakeException') ||
  76. errorStr.contains('Failed host lookup') ||
  77. errorStr.contains('Network is unreachable') ||
  78. errorStr.contains('Connection timed out') ||
  79. errorStr.contains('Connection closed') || // 补充:上个日志提到的错误
  80. errorStr.contains('Connection reset') || // 补充:被对方重置
  81. errorStr.contains('Unable to connect');
  82. if (isEnvironmentIssue) {
  83. _log.warning('已拦截环境异常: $errorStr');
  84. FirebaseCrashlytics.instance.log('Env Error (Ignored): $errorStr');
  85. return true;
  86. }
  87. // 2. 拦截插件缺失类异常
  88. if (error is MissingPluginException) {
  89. _log.warning('插件未找到: $errorStr');
  90. return true;
  91. }
  92. // 3. 剩下的才是逻辑错误(如 Null Check, Range Error)
  93. // 改为 fatal: false,避免降低 Google Play 的“崩溃率评分”
  94. _log.severe('未处理的逻辑错误', error, stack);
  95. FirebaseCrashlytics.instance.recordError(error, stack, fatal: false);
  96. return true;
  97. };
  98. } catch (e) {
  99. debugPrint("Firebase couldn't be initialized: $e");
  100. }
  101. }
  102. //本地参数存储初始化
  103. await Persistence().initialize();
  104. // 远程参数初始化
  105. RemoteConfig().initialize();
  106. // 记录程序运行时间
  107. Persistence().lastRunTime = DateTime.now();
  108. Directory baseDir = await getApplicationDocumentsDirectory();
  109. // 首次运行, 将json写入
  110. if (Persistence().firstRun) {
  111. final json = await loadJSONFromAsset('assets/builtin/${cfg.Config.firstId}.json');
  112. await saveJson('work/${cfg.Config.firstId}.json', json);
  113. }
  114. runApp(MyApp(baseDir: baseDir));
  115. }
  116. class MyApp extends StatelessWidget {
  117. final Directory baseDir;
  118. const MyApp({super.key, required this.baseDir});
  119. // This widget is the root of your application.
  120. @override
  121. Widget build(BuildContext context) {
  122. cfg.Config config = cfg.Config(context, baseDir);
  123. return AppLifecycleObserver(
  124. child: MultiProvider(
  125. providers: [
  126. Provider<Data>(lazy: false, create: (context) => Data(persistence: Persistence())..loadDataFromPersistence()),
  127. Provider<SettingsController>(lazy: false, create: (context) => SettingsController(persistence: Persistence())..loadStateFromPersistence()),
  128. // ProxyProvider2<SettingsController, ValueNotifier<AppLifecycleState>, AudioController>(
  129. // lazy: false,
  130. // create: (context) => AudioController()..initialize(),
  131. // update: (context, settings, lifecycleNotifier, audio) {
  132. // if (audio == null) throw ArgumentError.notNull();
  133. // audio.attachSettings(settings);
  134. // audio.attachLifecycleNotifier(lifecycleNotifier);
  135. // return audio;
  136. // },
  137. // dispose: (context, audio) => audio.dispose(),
  138. // ),
  139. ProxyProvider2<SettingsController, ValueNotifier<AppLifecycleState>, JcAudioController>(
  140. lazy: false,
  141. create: (context) => JcAudioController()..initialize(),
  142. update: (context, settings, lifecycleNotifier, audio) {
  143. if (audio == null) throw ArgumentError.notNull();
  144. audio.attachSettings(settings);
  145. audio.attachLifecycleNotifier(lifecycleNotifier);
  146. return audio;
  147. },
  148. dispose: (context, audio) => audio.dispose(),
  149. ),
  150. Provider<ApplovinAdsController>(create: (context) => ApplovinAdsController(context)),
  151. Provider<cfg.Config>(lazy: false, create: (context) => config),
  152. Provider<Device>(lazy: false, create: (context) => config.device),
  153. ],
  154. child: Prepare(
  155. child: MaterialApp(
  156. key: GlobalKey(),
  157. title: 'Jigsort Solitaire',
  158. // initialRoute: firstRun ? '/play' : '/', // 首次游戏直接进入游戏页面,而不是合集页
  159. initialRoute: '/', // 统一先到HomeScreen, 再根据情况跳转到相应页面
  160. navigatorObservers: [routeObserver],
  161. routes: {
  162. '/': (context) => const HomeScreen(),
  163. // '/': (context) => const GalleryScreen(),
  164. '/play': (context) => BoardPlay(
  165. item: AssetItem(
  166. cfg.Config.firstId,
  167. '',
  168. 2000,
  169. 3000,
  170. 3,
  171. false,
  172. 'assets/builtin/${cfg.Config.firstId}.jpeg',
  173. 'assets/builtin/${cfg.Config.firstId}.jpeg',
  174. ),
  175. firstRun: true,
  176. ),
  177. },
  178. theme: ThemeData(
  179. // textTheme: GoogleFonts.nunitoSansTextTheme(Theme.of(context).textTheme),
  180. brightness: Brightness.light,
  181. primaryColor: Colors.green,
  182. primarySwatch: Colors.blue,
  183. ),
  184. ///多语言设置
  185. localizationsDelegates: AppLocalizations.localizationsDelegates,
  186. supportedLocales: AppLocalizations.supportedLocales,
  187. /// 设置默认语言为英语
  188. localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales) {
  189. var result = supportedLocales.where((element) => element.languageCode == locale?.languageCode);
  190. if (result.isNotEmpty) {
  191. Device.locale = locale!;
  192. _log.info('当前语言(支持):${Device.locale!.countryCode}-${Device.locale!.languageCode}');
  193. return locale;
  194. }
  195. Device.locale = const Locale('en');
  196. _log.info('当前语言(默认):${Device.locale!.countryCode}-${Device.locale!.languageCode}');
  197. return Device.locale;
  198. },
  199. ),
  200. ),
  201. ),
  202. );
  203. }
  204. }
  205. class Prepare extends StatefulWidget {
  206. final Widget child;
  207. const Prepare({super.key, required this.child});
  208. @override
  209. State<Prepare> createState() => _PrepareState();
  210. }
  211. class _PrepareState extends State<Prepare> {
  212. @override
  213. void initState() {
  214. super.initState();
  215. loadDeviceInfo();
  216. }
  217. /// 获取android平台信息,用户判断是否低端机
  218. loadDeviceInfo() async {
  219. DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
  220. if (Platform.isAndroid) {
  221. try {
  222. context.read<Device>().androidDeviceInfo = await deviceInfoPlugin.androidInfo;
  223. } catch (e) {}
  224. }
  225. }
  226. @override
  227. Widget build(BuildContext context) {
  228. // applovin max 没有类似的api可以获取banner高度,以下代码注释掉
  229. //Update ad banner size
  230. // AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
  231. // MediaQuery.of(context).size.width.truncate(),
  232. // ).then((value) {
  233. // if (value != null) {
  234. // context.read<Device>().bannerHeight = value.height.toDouble();
  235. // }
  236. // }).catchError((err) {
  237. // //todo
  238. // });
  239. return widget.child;
  240. }
  241. }