import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:app_tracking_transparency/app_tracking_transparency.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:puzzleweave/ads/applovin_ads_controller.dart'; import 'package:puzzleweave/audio/jc_audio_controller.dart'; import 'package:puzzleweave/collection/collection_screen.dart'; import 'package:puzzleweave/config/config.dart'; import 'package:puzzleweave/config/device.dart'; import 'package:puzzleweave/firebase/adjust_helper.dart'; import 'package:puzzleweave/homepage/home_board_play.dart'; import 'package:puzzleweave/l10n/app_localizations.dart'; import 'package:puzzleweave/models/cached_request.dart'; import 'package:puzzleweave/models/download.dart'; import 'package:puzzleweave/models/items.dart'; import 'package:puzzleweave/persistence/persistence.dart'; import 'package:puzzleweave/play/board_play.dart'; import 'package:puzzleweave/settings/settings_screen.dart'; import 'package:puzzleweave/skin/skin.dart'; import 'package:puzzleweave/utils/mybutton.dart'; import 'package:puzzleweave/utils/utils.dart'; import '../ads/ad_helper.dart'; import '../ads/ads_state.dart'; import '../utils/memory_monitor.dart'; final Logger _log = Logger('home_screen'); class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreen(); } const int minimumRemoteLoadCount = 30; class _HomeScreen extends AdsState with TickerProviderStateMixin { late Device device; late JcAudioController audio; List? latest; late CachedRequest latestCachedRequest; late StreamSubscription? latestSubscription; final _canvasKey = GlobalKey(); final GlobalKey _collectionKey = GlobalKey(); bool isLoading = true; bool firstRun = false; // ✅ 优化点1: 导航状态管理 bool _isNavigating = false; // ✅ 优化点2: 防抖机制 Timer? _refreshDebouncer; // ✅ 优化点3: 缓存计算结果 double? _cachedCanvasWidth; double? _cachedCanvasHeight; bool _layoutCalculated = false; late AnimationController _collectionController; late Animation _collectionAnimation; bool interPending = false; @override void initState() { super.initState(); _log.info("首页初始化"); _initializeComponents(); _setupAnimations(); _handleInitialNavigation(); } void _initializeComponents() { device = context.read(); audio = context.read(); latestCachedRequest = data.latest; final cachedData = latestCachedRequest.cachedData; if (cachedData != null) { _onLatestDataUpdate(cachedData); } latestSubscription = latestCachedRequest.stream.listen(_onLatestDataUpdate, onError: _onLatestDataError); onInterstitialAdState = _createInterStateListener(); } Function(AdState state) _createInterStateListener() { return (AdState state) { _log.info('Interstitial ad state changed: $state'); if (state == AdState.dismissed && interPending) { _log.info('Interstitial ad dismissed, executing pending post-ad logic.'); _canvasKey.currentState?.startFlipAnimation(); final bool hasSufficientData = latest != null && latest!.length >= minimumRemoteLoadCount; final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch; if (hasSufficientData) { if (isNetworkActive) { _log.info('Game finished, data complete & Network Active. Triggering sequential preloading...'); _preloadNextImages(); } else { _log.info('Game finished, data complete but Network inactive. Attempting refresh.'); refresh(); } } else { _log.info('Game finished, remote data incomplete. Attempting refresh...'); refresh(); } interPending = false; } }; } void _setupAnimations() { _collectionController = AnimationController(duration: const Duration(milliseconds: 300), vsync: this) ..addStatusListener((status) { if (status == AnimationStatus.completed) { audio.playSfx(SfxType.pop); } }); _collectionAnimation = TweenSequence([ TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.4).chain(CurveTween(curve: Curves.easeOut)), weight: 40.0), TweenSequenceItem(tween: Tween(begin: 1.4, end: 1.0).chain(CurveTween(curve: Curves.easeIn)), weight: 60.0), ]).animate(_collectionController); } void _handleInitialNavigation() { if (Persistence().firstRun) { firstRun = true; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted && !_isNavigating) { _handleFirstRunNavigation(); } }); Persistence().firstRun = false; } WidgetsBinding.instance.addPostFrameCallback((_) { if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed && mounted) { audio.startMusic(); } }); } // ✅ 优化点4: 异步化首次运行导航 void _handleFirstRunNavigation() { if (_isNavigating) return; _log.info('First run detected, navigating to initial play page.'); final AssetItem initialItem = AssetItem( Config.firstId, '', 2000, 3000, 3, false, 'assets/builtin/${Config.firstId}.jpeg', 'assets/builtin/${Config.firstId}.jpeg', ); return gotoPlay(initialItem, firstRun: true); } // ✅ 优化点5: 异步化文件检查 void checkGoPlay() { if (currentItem == null || _isNavigating) return; Future.microtask(() async { try { final jsonFile = await localFile(currentItem!.jsonPath); final exists = await jsonFile.exists(); if (mounted && exists && !_isNavigating) { gotoPlay(currentItem!); } } catch (e) { _log.warning('Error checking play file: $e'); } }); } @override void dispose() { _refreshDebouncer?.cancel(); latestSubscription?.cancel(); _collectionController.dispose(); super.dispose(); } // ✅ 优化点6: 简化数据更新逻辑 _onLatestDataUpdate(datalist) { _log.info('_onLatestDataUpdate....'); if (datalist == null) return; bool shouldCheckGoPlay = false; if (currentItem == null && !firstRun) { shouldCheckGoPlay = true; } latest = datalist as List; isLoading = false; setState(() {}); final bool hasSufficientData = datalist.length >= minimumRemoteLoadCount; final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch; if (hasSufficientData) { if (!hasInit) { initThird(); } if (!isNetworkActive) { _debouncedRefresh(); } } else { _log.info('Data insufficient (only ${datalist.length} items). Attempting refresh.'); _debouncedRefresh(); } if (shouldCheckGoPlay) { checkGoPlay(); } } _onLatestDataError(error) { _log.info('_onLatestDataError.... $error'); if (latest == null || latest!.isEmpty || latest!.length < 20) { _log.warning("_onLatestDataError, retry again"); _debouncedRefresh(); } } // ✅ 优化点7: 防抖刷新 void _debouncedRefresh() { _refreshDebouncer?.cancel(); _refreshDebouncer = Timer(const Duration(seconds: 2), () { if (mounted) { refresh(); } }); } Future refresh() async { _log.info('refresh...'); await latestCachedRequest.refresh(); } ListItem? get currentItem { if (latest == null || latest!.isEmpty) { return null; } final Set completedIds = data.completedWorks.value.map((work) => work.id).toSet(); for (final item in latest!) { final String itemId = item.id; if (!completedIds.contains(itemId)) { _log.info('Found current item: $itemId'); return item; } } _log.info('All items in the latest list have been completed.'); return null; } // ✅ 优化点8: 后台预加载 void _preloadNextImages() { Future.microtask(() async { const int totalPreloadCount = 20; if (latest == null || latest!.isEmpty || latest!.length < minimumRemoteLoadCount) { _log.info('Preload failed: latest list is empty.'); return; } final Set completedIds = data.completedWorks.value.map((work) => work.id).toSet(); int startIndex = -1; for (int i = 0; i < latest!.length; i++) { if (!completedIds.contains(latest![i].id)) { startIndex = i; break; } } if (startIndex == -1) { _log.info('Preload: All images completed, nothing to preload.'); return; } final int endPreloadIndex = min(startIndex + totalPreloadCount, latest!.length); final List itemsToLoad = latest!.sublist(startIndex, endPreloadIndex); if (itemsToLoad.isEmpty) { _log.info('Preload: No items found in the range.'); return; } final ListItem currentItemToLoad = itemsToLoad.removeAt(0); itemsToLoad.add(currentItemToLoad); _log.info('Preloading ${itemsToLoad.length} images. Current item: ${currentItemToLoad.id} will be loaded last.'); int preloadCount = 0; for (final item in itemsToLoad) { if (item is RemoteItem) { try { ItemLoader.preload(item, device.suggestedQuality); await Future.delayed(const Duration(milliseconds: 100)); preloadCount++; } catch (e) { _log.warning('Failed to load item for preloading: ${item.id}, error: $e'); } } } _log.info('Preload initiated for $preloadCount remote images, current item was last.'); }); } // ✅ 优化点9: 缓存布局计算 void _calculateLayout() { if (_layoutCalculated) return; final double availableHeight = device.screenSize.height - device.appBarHeight - device.bannerHeight - 120; final double paddedWidth = device.screenSize.width - 2 * 30; final double paddedHeight = availableHeight; final double targetWidth = paddedWidth; final double targetHeight = targetWidth * device.aspectRatio; if (targetHeight > paddedHeight) { _cachedCanvasHeight = paddedHeight; _cachedCanvasWidth = paddedHeight / device.aspectRatio; } else { _cachedCanvasWidth = targetWidth; _cachedCanvasHeight = targetHeight; } _layoutCalculated = true; } @override Widget build(BuildContext context) { if (isLoading) return scrollableDummy; _calculateLayout(); return Scaffold( backgroundColor: SkinHelper.colorWhite, appBar: AppBar( backgroundColor: SkinHelper.colorWhite, centerTitle: true, leading: RepaintBoundary( key: _collectionKey, child: ScaleTransition( scale: _collectionAnimation, child: IconButton( onPressed: () { audio.playSfx(SfxType.click); Navigator.push(context, CollectionScreen.buildRoute()); }, icon: const Icon(Icons.collections, color: Colors.black87), ), ), ), title: SvgPicture.asset( 'assets/images/title.svg', height: 32, placeholderBuilder: (BuildContext context) => const Text( 'Jigsort Solitaire', style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 24), ), ), actions: [ IconButton( onPressed: () { audio.playSfx(SfxType.click); Navigator.push(context, SettingScreen.buildRoute()); }, icon: const Icon(Icons.settings, color: Colors.black87), ), ], ), body: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (Config.isDebug) MemoryMonitor.getMemoryWidget(), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 30), child: SizedBox( width: _cachedCanvasWidth!, height: _cachedCanvasHeight!, child: ValueListenableBuilder( valueListenable: data.completedWorks, builder: (context, value, child) { return HomeBoardPlay( key: _canvasKey, canvasWidth: _cachedCanvasWidth!, canvasHeight: _cachedCanvasHeight!, collectionKey: _collectionKey, onCollectionDone: () { _log.info('onCollectionDone, 启动合集收纳反馈动画'); audio.playSfx(SfxType.appear); _collectionController.forward(from: 0.0); }, ); }, ), ), ), playButton, ], ), ), SafeArea( child: SizedBox( height: context.read().bannerHeight, width: double.infinity, child: isBannerVisible ? getBanner('home_bottom') : const SizedBox.shrink(), ), ), ], ), ); } // ✅ 优化点10: 导航状态管理 void gotoPlay(ListItem item, {bool firstRun = false}) async { if (_isNavigating) { _log.info('Navigation already in progress, ignoring'); return; } _log.info('goto play, firstRun = $firstRun'); if (!mounted) return; _isNavigating = true; interPending = false; try { cleanBanner(); if (!mounted) return; PageRouteBuilder? pageRouteBuilder = BoardPlay.buildRoute(item, firstRun: firstRun); final result = await Navigator.push(context, pageRouteBuilder); if (!mounted) return; // result 是 true, 说明关卡已经完成。 但此时可能播放插屏广告中, 需要等待用户关闭广告之后才能执行翻牌等逻辑 if (result != null && result == true) { // 打印下当下的插屏广告状态 _log.info('==================>interState = $intersState'); if (intersState == AdState.ready && data.currentLevel % 25 != 0 && shouldShowInterstitialAd("level_done", data.currentLevel - 1)) { // 这种情况表示有插屏广告在播放,需要等待广告关闭之后才能执行翻牌动画等逻辑 interPending = true; _log.info('Interstitial ad is currently playing, will execute post-ad logic after dismissal.'); return; } _canvasKey.currentState?.startFlipAnimation(); final bool hasSufficientData = latest != null && latest!.length >= minimumRemoteLoadCount; final bool isNetworkActive = latestCachedRequest.hasRecentSuccessfulFetch; if (hasSufficientData) { if (isNetworkActive) { _log.info('Game finished, data complete & Network Active. Triggering sequential preloading...'); _preloadNextImages(); } else { _log.info('Game finished, data complete but Network inactive. Attempting refresh.'); refresh(); } } else { _log.info('Game finished, remote data incomplete. Attempting refresh...'); refresh(); } } } finally { _isNavigating = false; } } Widget get playButton { return MyElevatedButton( width: device.isTablet ? 300 : 200, height: 70, borderRadius: BorderRadius.circular(20), gradient: LinearGradient(colors: [SkinHelper.coreBgColor, SkinHelper.slotBorderColor]), onPressed: () async { audio.playSfx(SfxType.click); if (currentItem != null) { gotoPlay(currentItem!); } else { Fluttertoast.showToast( msg: AppLocalizations.of(context)!.noMorePicture, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIosWeb: 1, backgroundColor: SkinHelper.slotBorderColor, textColor: Colors.white, fontSize: 16.0, ); } }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( AppLocalizations.of(context)!.play, style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold), ), Text('${AppLocalizations.of(context)!.level} ${data.currentLevel + 1}', style: const TextStyle(color: Colors.white, fontSize: 16)), ], ), ); } Widget get scrollableDummy => Scaffold( backgroundColor: SkinHelper.colorWhite, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox(width: 40, height: 40, child: CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation(SkinHelper.coreBgColor))), const SizedBox(height: 20), Text( "Loading...", style: TextStyle(color: SkinHelper.slotBorderColor.withOpacity(0.7), fontSize: 14, fontWeight: FontWeight.w500), ), ], ), ), ); ///////////////////////// 初始化相关 ///////////////////////// static bool hasInit = false; void initThird() async { if (hasInit) return; hasInit = true; TrackingStatus attStatus = await AppTrackingTransparency.trackingAuthorizationStatus; if (attStatus == TrackingStatus.authorized && Platform.isIOS) { // ATT 通过之后,ios需要调用相关的原生sdk接口做进一步的初始化 } initFCM(); initAd(); AdjustHelper.init(Persistence().uuid); final idfa = await AppTrackingTransparency.getAdvertisingIdentifier(); _log.info("idfa: $idfa"); } Future initATT() async { TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus; _log.info('initATT111 $status'); if (status == TrackingStatus.notDetermined) { status = await AppTrackingTransparency.requestTrackingAuthorization(); _log.info('initATT222 $status'); } return status == TrackingStatus.authorized; } initAd() { _log.info('initAd'); ApplovinAdsController applovinAdsController = context.read(); applovinAdsController.initialize(); } initFCM() async { try { final fcmToken = await FirebaseMessaging.instance.getToken(); _log.info("FCM Token: $fcmToken"); FirebaseMessaging messaging = FirebaseMessaging.instance; NotificationSettings settings = await messaging.requestPermission( alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true, ); _log.warning('User granted permission: ${settings.authorizationStatus}'); } catch (e) { FirebaseCrashlytics.instance.log("FCM FirebaseMessaging.instance.getToken error: $e"); _log.warning(e); } } }